惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Schneier on Security
F
Fortinet All Blogs
博客园_首页
The GitHub Blog
The GitHub Blog
V
Visual Studio Blog
D
DataBreaches.Net
aimingoo的专栏
aimingoo的专栏
爱范儿
爱范儿
B
Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
N
Netflix TechBlog - Medium
阮一峰的网络日志
阮一峰的网络日志
P
Proofpoint News Feed
D
Docker
Engineering at Meta
Engineering at Meta
大猫的无限游戏
大猫的无限游戏
The Cloudflare Blog
罗磊的独立博客
云风的 BLOG
云风的 BLOG
Microsoft Azure Blog
Microsoft Azure Blog
T
The Exploit Database - CXSecurity.com
博客园 - 三生石上(FineUI控件)
量子位
The Last Watchdog
The Last Watchdog
MyScale Blog
MyScale Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
The Blog of Author Tim Ferriss
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
小众软件
小众软件
Cloudbric
Cloudbric
博客园 - 司徒正美
H
Help Net Security
人人都是产品经理
人人都是产品经理
Application and Cybersecurity Blog
Application and Cybersecurity Blog
L
LangChain Blog
Latest news
Latest news
M
MIT News - Artificial intelligence
T
Threat Research - Cisco Blogs
博客园 - Franky
S
Security Affairs
W
WeLiveSecurity
F
Full Disclosure
Know Your Adversary
Know Your Adversary
Google DeepMind News
Google DeepMind News
The Hacker News
The Hacker News
Cyberwarzone
Cyberwarzone
美团技术团队
PCI Perspectives
PCI Perspectives
C
Check Point Blog
Spread Privacy
Spread Privacy

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
WebAssembly как платформа расширений для Python: ускорение, встраивание и опасные ловушки API
kmoseenk (OT · 2026-04-30 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение12 мин

Охват и читатели762

Туториал

Перевод

Программное обеспечение, достигающее определённого уровня сложности, как правило, обзаводится языком расширений и фактически превращается в самостоятельную программную платформу. 

В этой роли хорошо себя показывает Lua, а для веб-технологий, разумеется, используется JavaScript. WebAssembly обобщает этот подход: любой язык программирования, способный компилироваться в Wasm, может расширять приложение, выполняющее Wasm. Это требует больше усилий, чем просто передать скрипт в текстовом файле, зато авторы расширений могут писать на удобном им языке и использовать более развитые инструменты разработки — отладки, тестирования и прочего — которые обычно недоступны для традиционных языков расширений.

Традиционно Python расширяется с помощью нативного кода через интерфейс на C, однако в последнее время стало практично расширять Python с помощью Wasm. Это означает, что можно поставлять бинарный модуль (blob) Wasm, не зависящий от архитектуры, внутри Python-библиотеки и использовать его без необходимости иметь на целевой системе нативную цепочку инструментов. Рассмотрим два разных сценария использования и связанные с ними подводные камни.

Чтобы понять, какие темы в Python стоит подтянуть перед работой с расширениями, памятью и внешними модулями, можно пройти короткий тест и оценить свой текущий уровень.

Обычно Python расширяют, чтобы получить доступ к внешним интерфейсам, которые он сам по себе использовать не может. Wasm выполняется в песочнице без какого-либо доступа к внешнему миру, поэтому для этой задачи он, очевидно, не подходит. Однако расширения могут также ускорять Python — это одно из ключевых преимуществ Wasm. Кроме того, с его помощью можно подключать встраиваемые возможности, написанные на других языках программирования, если им не требуется доступ к внешней среде.

В качестве предпочтительной среды выполнения Wasm (без поддержки WASI) можно использовать wasm3. Он написан на классическом C и хорошо подходит для встраивания, примерно так же, как, например, SQLite. Производительность у него средняя, однако программа на C, выполняемая в wasm3, всё равно заметно быстрее аналогичной программы на Python. Для него есть Python-привязки — pywasm3, но они распространяются только в виде исходного кода. 

Это означает, что на машине должен быть установлен компилятор C, чтобы использовать pywasm3, что противоречит моим целям. Если на системе уже есть такая цепочка инструментов, проще сразу использовать её, минуя Wasm.

Для рассматриваемых в этой статье сценариев лучшим вариантом является wasmtime-py. Дистрибутив включает готовые бинарные файлы для Windows, macOS и Linux на архитектурах x86-64 и ARM64, что покрывает практически все установки Python. На стороне пользователя требуется только интерпретатор Python — никаких нативных инструментов сборки. 

Это почти так же удобно, как если бы Wasm был встроен в сам Python. В моих тестах он работает в 3–10 раз быстрее, чем wasm3, поэтому для первого сценария ситуация даже лучше.

Однако есть и недостатки: установленный пакет занимает около 18 МБ и, вероятно, со временем будет сопоставим по размеру с самим интерпретатором Python. Кроме того, его API меняется практически каждый месяц, так что вам придётся постоянно обновляться, иначе через пару лет ваш код может устареть и перестать работать. В этой статье рассматривается версия 40.

Примеры использования и подводные камни

В официальных примерах нет ничего нетривиального или по-настоящему интересного, поэтому, чтобы во всём разобраться, мне пришлось изучать документацию, а в ней не так уж много подсказок. Базовая настройка выглядит так:

import functools
import wasmtime

store    = wasmtime.Store()
module   = wasmtime.Module.from_file(store.engine, "example.wasm")
instance = wasmtime.Instance(store, module, ())
exports  = instance.exports(store)

memory = exports["memory"].get_buffer_ptr(store)
func1  = functools.partial(exports["func1"], store)
func2  = functools.partial(exports["func2"], store)
func3  = functools.partial(exports["func3"], store)

Store — это область выделения памяти, из которой создаются все объекты Wasm. Освобождать отдельные объекты нельзя, можно только отбросить весь store целиком. Честно говоря, это вполне разумно. Менее разумно другое: чтобы воспользоваться объектами, мне снова и снова приходится передавать store в каждый из них. Эти объекты связаны ровно с одним store и не могут использоваться с другим. Передадите не тот store — и всё аварийно завершится. Хотя он и так уже отслеживается внутри. 

Почему интерфейс устроен именно так, я не понимаю. Поэтому, чтобы упростить себе жизнь, я использую functools.partial, привязывая параметр store и получая интерфейс в том виде, в каком я его ожидаю.

Объект get_buffer_ptr реализует протокол буфера, и если вы передаёте что-то кроме байтов, то, скорее всего, именно его и стоит использовать для доступа к памяти. Для него действуют обычные оговорки: если вы меняете размер памяти, то, вероятно, стоит заново получить объект буфера. Для байтов, например буферов и строк, я предпочитаю методы read и write.

Поскольку поддержка возврата и передачи нескольких значений (multi-value) в экосистеме Wasm всё ещё остаётся экспериментальной, структуры через Wasm вы, скорее всего, передавать не будете. Всё, что сложнее скалярных значений, потребует указателей и копирования данных в линейную память Wasm и обратно. Здесь возникает типичная ловушка, в которую попадаются почти все: интерфейсы Wasm не различают указатели и целые числа, а среды выполнения Wasm обычно интерпретируют все целые числа как знаковые. 

То есть ваши указатели тоже будут считаться знаковыми, если вы специально ничего не предпримете. А адреса начинаются с нуля, так что это очень, очень плохая новость.

malloc = functools.partial(exports["func1"], store)

hello = b"hello"
pointer = malloc(len(hello))
assert pointer
memory = exports["memory"].write(store, hello, pointer)  # НЕПРАВИЛЬНО!

И этого мало: в wasmtime-py есть ещё одна неприятная ловушка. Методы read и write следуют сомнительному соглашению Python, по которому отрицательные индексы отсчитываются от конца. Если malloc вернёт указатель из верхней половины памяти, отрицательный указатель пройдёт проверку границ внутри write, потому что отрицательные индексы там допустимы, и данные будут молча записаны не по тому адресу. Вот так сюрприз.

Мне стало интересно, насколько распространена эта ошибка, и я поискал примеры в сети. Мне удалось найти только один нетривиальный случай использования wasmtime-py в реальном проекте — в изолированном просмотрщике PDF. И, как я и ожидал, там попались в ловушку с отрицательным указателем. Более того, это ещё и переполнение буфера с записью в область памяти Python:

buf_ptr = malloc(store, len(pdf_data))
mem_data = memory.data_ptr(store)

for i, byte in enumerate(pdf_data):
    mem_data[buf_ptr + i] = byte

Метод data_ptr возвращает сырой указатель ctypes без проверки границ, так что на деле здесь сразу две ошибки. Во-первых, если разработчика вообще заботит работа в песочнице, доверять указателям, пришедшим из Wasm, не следует. Во-вторых, остаётся риск отрицательного указателя, а в таком случае запись произойдёт за пределами памяти Wasm, уже в памяти Python, что, будем надеяться, хотя бы приведёт к segfault.

Что с этим делать? Каждый указатель, приходящий из Wasm, нужно усекать маской:

pointer = malloc(...) & 0xffffffff   # правильно для wasm32!

Так результат интерпретируется как беззнаковый. Для 64-битного Wasm нужна 64-битная маска, хотя на практике получить корректный отрицательный указатель из 64-битного Wasm вы не сможете. Это правило относится и к JavaScript, где обычно используют такой приём:

let pointer = malloc(...) >>> 0

Среды выполнения Wasm тут помочь не могут — у них просто нет необходимой информации. Возможно, это фундаментальный изъян в самом устройстве Wasm. Но как только вы узнаёте об этой проблеме, начинаете замечать её буквально повсюду.

Когда у вас уже есть корректный адрес, можно использовать его с представлением памяти через протокол буфера. Если вы работаете с NumPy, есть разные способы взаимодействовать с этой памятью, оборачивая её в типы NumPy, но только если хост использует порядок байтов little-endian. Если же у вас машина с big-endian, то от запуска Wasm, пожалуй, лучше сразу отказаться.

Первый сценарий, который я имею в виду, обычно сводится к копированию обычных значений Python туда и обратно. Здесь очень удобен пакет struct:

vec2   = malloc(...) & 0xffffffff
memory = exports["memory"].get_buffer_ptr(store)
struct.pack_into("<ii", memory, vec2, x, y)

Он играет примерно ту же роль, что и DataView в JavaScript. Если нужно копировать много чисел, то в CPython быстрее сформировать собственную строку формата, чем использовать цикл:

nums: list[int] = ...
struct.pack_into(f"<{len(nums)}i", memory, buf, *nums)

Чтобы извлечь структуры обратно, используйте struct.unpack_from. Если вы передаёте строки, придётся вызывать .encode() и .decode(), чтобы преобразовывать их в байты и обратно, а байты хорошо подходят для работы через read и write.

На практике в реальных Wasm-программах вы, скорее всего, будете взаимодействовать с гостевым аллокатором (guest allocator) извне, запрашивая память, в которую затем копируются входные данные для функции. В моих примерах используется malloc, потому что он не требует дополнительных пояснений, но, как это часто бывает, bump аллокатор решает задачу гораздо лучше, особенно потому, что не требует тащить в Wasm-программу полноценный аллокатор общего назначения. 

Достаточно одной глобальной арены памяти: никакие другие потоки не будут совместно использовать этот экземпляр Wasm, так что можно быстро выполнять столько выделений памяти, сколько нужно, вообще не думая об управлении памятью на стороне хоста, затем вызвать функцию, которая при необходимости выделит результат из той же арены, а потом просто сбросить арену и очистить всё разом. По сути, это стек для передачи значений туда и обратно.

WebAssembly как более быстрый Python

Предположим, что в нашей Python-программе мы нашли вычислительно затратный участок внутри функции, написанной на чистом Python, то есть без вызова расширения. Такую функцию имеет смысл оптимизировать. По моим экспериментам, если переписать её на C, скомпилировать в Wasm и затем выполнять этот фрагмент Wasm вместо исходной функции, можно рассчитывать примерно на десятикратное ускорение.

В общем случае C обычно быстрее Python примерно в сто раз, а накладные расходы на взаимодействие с Wasm — копирование данных туда и обратно и прочее — могут быть заметными, но не настолько большими, чтобы всё это теряло смысл. Выигрыш становится ещё больше, если можно изменить интерфейс, например потребовать от вызывающей стороны использовать протокол буфера.

Благодаря wasmtime-py я могу внедрить такое решение без возни с кросс-компиляторами для сборки бинарных пакетов под разные платформы и без требования наличия цепочки инструментов на целевой системе. Нужен лишь довольно тяжёлый Python-пакет. Возможно, оно того стоит.

Мой основной экспериментальный тест — это вариация моего решения задачи «Two Sum», которое я изначально написал для JavaScript, затем адаптировал под pywasm3, а позже и под wasmtime-py. Пример простой, но достаточно показательный и хорошо отражает тот тип Wasm-встраивания, который я имею в виду. Интерфейс остаётся тем же, но реализация выполняется через Wasm.

# Исходный интерфейс в стиле Python
def twosum(nums: list[int], target: int) -> tuple[int, int] | None:
   ...

# Интерфейс Wasm с состоянием
class TwoSumWasm():
   def __init__(self):
       store    = wasmtime.Store()
       module   = wasmtime.Module.from_file(store.engine, ...)
       instance = wasmtime.Instance(store, module, ())
       ...

   def twosum(self, nums, target):
       # ... использовать экземпляр wasm ...

Здесь появляется состояние, связанное с самим экземпляром Wasm. Если скрыть его, сделав глобальным, придётся синхронизировать доступ к нему между потоками. В многопоточной программе, возможно, стоит использовать локальные для потока данные, создаваемые по требованию. Мне пока не приходилось это решать.

Однако здесь особенно заметна слабость store в wasmtime: обратите внимание, что компиляция и создание экземпляра связаны в рамках одного store. Я не могу один раз скомпилировать модуль, а затем по мере необходимости создавать временные экземпляры, например как это нужно при каждом запуске программы WASI. Каждый новый экземпляр навсегда расширяет store, в котором происходила компиляция. На практике это означает, что для каждого такого временного экземпляра нам приходится заново и совершенно нерационально перекомпилировать Wasm-программу.

Несмотря на внешний вид API, компиляция и создание экземпляра здесь на самом деле не являются отдельными этапами, как это сделано в Wasm API для JavaScript. wasmtime.Instance принимает store первым аргументом, и это наводит на мысль, что для создания экземпляра можно использовать другой store. Это решило бы проблему, но на момент написания статьи требуется тот же самый store, который использовался при компиляции модуля. Для некоторых реальных сценариев это критический изъян, особенно для WASI.

Обновление: Вольфганг Майер подсказал методы serialize и deserialize, которые позволяют отвязать скомпилированный модуль от его store и тем самым создавать независимые экземпляры. Я попробовал этот подход, и он оказался вполне рабочим обходным решением. Накладные расходы невелики, а при десериализации валидация не выполняется. В моём тесте теперь используется именно этот способ — на будущее, поскольку я ожидаю, что это будет мой типичный сценарий.

WebAssembly как встраиваемые возможности

Monocypher — замечательная криптографическая библиотека. Она компактная, эффективная и хорошо подходит для встраивания, настолько хорошо, что распространяется в виде единого объединённого файла. Ей не нужны ни libc, ни среда выполнения, поэтому её можно напрямую скомпилировать в Wasm почти любой цепочкой инструментов Clang:

$ clang --target=wasm32 -nostdlib -O2 -Wl,--no-entry -Wl,--export-all
        -o monocypher.wasm monocypher.c

Эта библиотека не «понимает» Wasm, поэтому мне нужен флаг --export-all, чтобы открыть наружу весь интерфейс. И это даже удобно, потому что в рамках одной единицы трансляции всё, что имеет внешнее связывание, и становится интерфейсом.

Но помните, что я говорил о взаимодействии с гостевым аллокатором? Здесь никакого распределителя нет, и так и должно быть. В таком виде библиотека не слишком удобна для использования, потому что память пришлось бы полностью управлять извне. Это решаемо, но улучшить ситуацию легко: достаточно добавить ещё пару функций, по-прежнему оставаясь в пределах одной единицы трансляции:

#include "monocypher.c"

extern char  __heap_base[];
static char *heap_used;
static char *heap_high;

void *bump_alloc(ptrdiff_t size)
{
    // ...
}

void bump_reset()
{
    ptrdiff_t len = heap_used - __heap_base;
    __builtin_memset(__heap_base, 0, len);  // стереть ключи и т. п.
    heap_used = __heap_base;
}

О __heap_base я уже писал раньше: это часть ABI. Мы будем складывать ключи, входные данные и прочее в этот «стек», запускать нашу криптографическую процедуру, копировать наружу результат, а затем сбрасывать bump аллокатор, который при этом затирает все чувствительные данные. Нередко одного memset недостаточно: обычно сначала память зануляется, а потом освобождается, и компилятор видит, что время жизни объекта подходит к концу.

Но здесь никакое время жизни не заканчивается, а записи в эту область «кучи» с точки зрения абстрактной машины остаются наблюдаемыми извне. Иначе мы бы не смогли надёжно копировать наружу результаты.

В этом API много всего, но я остановлюсь только на интерфейсе AEAD. Мы «запираем» данные в зашифрованный контейнер и можем записать снаружи любую незашифрованную метку. Позже этот контейнер можно открыть, но только если ни его содержимое, ни эта метка не были подменены. Очень добротный дизайн API:

void crypto_aead_lock(uint8_t       *cipher_text,
                      uint8_t        mac  [16],
                      const uint8_t  key  [32],
                      const uint8_t  nonce[24],
                      const uint8_t *ad,         size_t ad_size,
                      const uint8_t *plain_text, size_t text_size);
int crypto_aead_unlock(uint8_t       *plain_text,
                       const uint8_t  mac  [16],
                       const uint8_t  key  [32],
                       const uint8_t  nonce[24],
                       const uint8_t *ad,          size_t ad_size,
                       const uint8_t *cipher_text, size_t text_size);

Скомпилировав это в Wasm, мы можем получить доступ к этой функциональности из Python почти так, будто она написана на чистом Python, и взаимодействовать с другими системами, использующими Monocypher.

Поскольку Monocypher сам по себе не взаимодействует с внешним миром, он полагается на вызывающую сторону, которая должна использовать системный криптографически стойкий генератор случайных чисел (CSPRNG) для создания этих nonce и ключей. Мы сделаем это с помощью встроенного пакета secrets:

class Monocypher:
    def __init__(self):
        ...
        self._read   = functools.partial(memory.read, store)
        self._write  = functools.partial(memory.write, store)
        self.__alloc = functools.partial(exports["bump_alloc"], store)
        self._reset  = functools.partial(exports["bump_reset"], store)
        self._lock   = functools.partial(exports["crypto_aead_lock"], store)
        self._unlock = functools.partial(exports["crypto_aead_unlock"], store)
        self._csprng = secrets.SystemRandom()

    def _alloc(self, n):
        return self.__alloc(n) & 0xffffffff

    def generate_key(self):
        return self._csprng.randbytes(32)

    def generate_nonce(self):
        return self._csprng.randbytes(24)

    ...

С надёжной основой всё остальное уже делается легко. Блок finally гарантирует, что секретные данные всегда будут удалены из памяти Wasm, а всё остальное сводится к копированию байтов туда и обратно:

def aead_lock(self, text, key, ad = b""):
        assert len(key) == 32
        try:
            macptr   = self._alloc(16)
            keyptr   = self._alloc(32)
            nonceptr = self._alloc(24)
            adptr    = self._alloc(len(ad))
            textptr  = self._alloc(len(text))

            self._write(key, keyptr)
            nonce = self.generate_nonce()
            self._write(nonce, nonceptr)
            self._write(ad,    adptr)
            self._write(text,  textptr)

            self._lock(
                textptr,
                macptr,
                keyptr,
                nonceptr,
                adptr, len(ad),
                textptr, len(text),
            )
            return (
                self._read(macptr, macptr+16),
                nonce,
                self._read(textptr, textptr+len(text)),
            )
        finally:
            self._reset()

А aead_unlock устроен почти так же, только в обратном порядке. Если контейнер не удаётся открыть, например из-за подмены данных, метод выбрасывает исключение:

def aead_unlock(self, text, mac, key, nonce, ad = b""):
        assert len(mac) == 16
        assert len(key) == 32
        assert len(nonce) == 24
        try:
            macptr   = self._alloc(16)
            keyptr   = self._alloc(32)
            nonceptr = self._alloc(24)
            adptr    = self._alloc(len(ad))
            textptr  = self._alloc(len(text))

            self._write(mac, macptr)
            self._write(key, keyptr)
            self._write(nonce, nonceptr)
            self._write(ad, adptr)
            self._write(text, textptr)

            if self._unlock(
                textptr,
                macptr,
                keyptr,
                nonceptr,
                adptr, len(ad),
                textptr, len(text),
            ):
                raise ValueError("AEAD mismatch")
            return self._read(textptr, textptr+len(text))
        finally:
            self._reset()

Использование:

mc = Monocypher()
key = mc.generate_key()
message = "Hello, world!"
mac, nonce, encrypted = mc.aead_lock(message.encode(), key)

Передайте mac, nonce и encrypted другой стороне, или самому себе в будущем, если ключ у вас уже есть:

decrypted = mc.aead_unlock(encrypted, mac, key, nonce)

Полный исходный код можно найти в моём рабочем репозитории.

Если тема встраивания чужого кода, памяти, указателей и Python-инфраструктуры вам близка, можно продолжить разбор на смежных бесплатных уроках:

  • 6 мая в 20:00. «Rust в деле: пишем многопользовательский чат с сервером, клиентом и CLI». Записаться

  • 7 мая в 20:00. «Настройка удобного рабочего окружения для Python проекта». Записаться

  • 21 мая в 20:00. «Перестаньте бояться указателей: как Go экономит вашу память и CPU». Записаться

Еще больше практических демо-уроков по разработке и не только можно найти в календаре мероприятий.