Понадобилось научить десктопную программу принимать лицензионные ключи. Первое, что приходит на ум: волшебная строка из множества несвязанных, на первый взгляд, между собой символов: человек получает её в письме, вставляет в окно программы, и вуа-ля всё работает, интернет для этого в общем то и не нужен. Проверка без сервера означает криптографию с открытым ключом: приватным ключом подписываю я у себя, публичный лежит внутри приложения и умеет только проверять.
Дальше я сделал ровно то, что сделал бы наверное почти каждый: пошёл за Ed25519. Он компактный, быстрый, детерминированный, лежит в каждом втором SSH-ключе. В стандартной библиотеке .NET его не оказалось: ни в восьмой версии, ни в десятой. Зато в десятой уже есть постквантовые ML-DSA и SLH-DSA.
Дальше:
что показали замеры;
почему я в итоге выбрал самый медленный по проверке алгоритм из трёх;
почему не жалею об этом.
Что вообще требовалось от подписи?
Задача кажется academic только на первый взгляд. Требования диктовала не криптография, а форма продукта.
Требование |
Откуда взялось |
|---|---|
Проверка без сети |
лицензия должна работать на компьютере без интернета |
Ноль сторонних зависимостей |
проверку лицензии хочу собрать из того, что уже есть в платформе |
Ключ не должен превращаться в простыню |
его вставляют руками, из письма |
Ротация ключей |
если приватный ключ утечёт, нужно уметь выпустить новый, не ломая старые лицензии |
Проверка не тормозит запуск |
она происходит не один раз за сеанс |
Последний пункт стоит пояснить, потому что он не так очевиден. Я сознательно не стал делать в программе единый флаг «пользователь оплатил»: такой флаг снимается одним хак патчем в памяти. Для защиты от этого подпись перепроверяется в каждой точке, где платная функция реально исполняется. То есть проверок за сеанс не одна, а десятки, и вопрос «сколько стоит одна проверка» превращается из теоретического в практический.
Идёшь за Ed25519 — и упираешься в рантайм
Первым делом я полез проверять, что вообще есть в System.Security.Cryptography. Проверять решил не по памяти и не по статьям — рефлексией по самой сборке, в двух версиях рантайма сразу.
Факт-чек: что рантайм выставляет наружу
var asm = typeof(ECDsa).Assembly; Console.WriteLine(asm.GetName().Name + " " + asm.GetName().Version); // какие асимметричные примитивы вообще доступны foreach (var t in asm.GetExportedTypes()) if (t.IsAbstract && typeof(AsymmetricAlgorithm).IsAssignableFrom(t)) Console.WriteLine(t.Name); // какие кривые библиотека знает по имени foreach (var p in typeof(ECCurve.NamedCurves).GetProperties()) Console.WriteLine(p.Name);
Результат одинаково скучный и одинаково показательный в обеих версиях.
Что искали |
.NET 8.0.28 |
.NET 10.0.9 |
|---|---|---|
Типы со словом «25519» |
нет ни одного |
нет ни одного |
Асимметричные примитивы |
DSA, ECAlgorithm, ECDiffieHellman, ECDsa, RSA |
ровно те же |
Именованные кривые |
nistP256, nistP384, nistP521 и 14 brainpool |
ровно те же |
Постквантовая подпись |
нет |
ML-DSA, SLH-DSA, ML-KEM, композитные |
Попытка получить кривую по имени вежливо объясняет, что мне здесь не рады. Формулировка, кстати, лукавая: дело не в «этой платформе», а в том, что такого примитива нет в API вообще.
Ответ на попытку создать Ed25519
ECDsa.Create(ECCurve.CreateFromFriendlyName("Ed25519")) → PlatformNotSupportedException: The specified curve ‘Ed25519’ or its parameters are not valid for this platform.
Дальше я полез в трекер рантайма — и там оказалось интереснее, чем в коде. Предложение добавить Ed25519 и Curve25519 создано 19 июня 2015 года. Актуальное предложение API висит с 28 декабря 2021 года, у него 53 комментария, метка api-approved и веха 11.0.0. То есть API согласован, но появится он в одиннадцатой версии, а мне подписывать ключи нужно было в восьмой.
Вот это поворот: подпись, устойчивую к квантовому компьютеру, в рантайм завезли раньше, чем ту, которой пятнадцать лет и которая лежит у половины читателей в ~/.ssh!
Что сложного: «просто возьми пакет»?
Очевидное возражение: возьми библиотеку и не занимайся ерундой. Я и попробовал встроенный и наиболее распространённые.
Вариант |
Что приезжает вместе с ним |
|---|---|
Встроенный ECDSA |
ноль байт, он уже в рантайме |
BouncyCastle 2.6.2 |
одна управляемая сборка 4,70 МБ, работает везде одинаково |
NSec 24.4.0 |
обёртка 92 КБ плюс нативная libsodium под каждую платформу: 10 файлов, суммарно 4,08 МБ |
С NSec выяснилась деталь, которая мне “понравилась” даже больше цифр. Свежая версия пакета отказалась ставиться в проект на .NET 8:
Ответ NuGet на попытку взять последнюю версию
error NU1202: Пакет NSec.Cryptography 26.4.0 несовместим с net8.0. Пакет NSec.Cryptography 26.4.0 поддерживает: net9.0, net9.0-ios18.0, ...
Ничего криминального, обычная жизнь библиотеки. Но это ровно то, о чём стоит подумать заранее: у зависимости свой график, и он не совпадает с вашим. Сегодня вы на восьмёрке и всё хорошо, завтра нужен фикс, а фикс вышел, например только для девятки.
З.Ы. по Фрейду. Мой дистрибутив весит 63,7 МБ, потому что собран со встроенным рантаймом. На этом фоне лишние 4,7 МБ — это 7 %, и логически возникающий аргумент против: «раздувает дистрибутив» не применим в данной ситуации. Почему? Встроенная криптография в дистрибутиве уже лежит на машине пользователя как часть рантайма: взяв её, я не добавляю в проверку лицензии ни одного нового и не понятного “участника”. Сторонний пакет — это, наоборот, ещё один объект, за которым надо следить: обновлять, смотреть его уязвимости, помнить про его собственный график поддержки платформ (что мы только что и увидели) и проверять, как он переживёт планируемую обфускацию — сторонние сборки на ней спотыкаются охотнее. Мне не хотелось бы однажды разбираться, почему платный уровень перестал включаться после обновления чужой библиотеки.
Замеры: кто быстрее?
Хватит теории и скучной лирики — берём и меряем. Я взял четыре варианта и прогнал на своей рабочей машине: AMD Ryzen 7 5800H, Windows 10, .NET 8.0.28, сборка Release. Полезная нагрузка — 39 байт, ровно столько в моём формате ключа. Прогрев 2 000 итераций, замер 20 000, лучший результат из пяти прогонов.
Как мерил (сокращённо)
var payload = RandomNumberGenerator.GetBytes(39); using var ec = ECDsa.Create(ECCurve.NamedCurves.nistP256); var sig = ec.SignData(payload, HashAlgorithmName.SHA256); for (int i = 0; i < 2000; i++) // прогрев JIT ec.VerifyData(payload, sig, HashAlgorithmName.SHA256); var sw = Stopwatch.StartNew(); for (int i = 0; i < 20000; i++) ec.VerifyData(payload, sig, HashAlgorithmName.SHA256); sw.Stop(); Console.WriteLine(sw.Elapsed.TotalMilliseconds / 20000 + " мс на проверку");
Алгоритм |
Подпись |
Проверка |
Размер подписи |
Публичный ключ |
|---|---|---|---|---|
ECDSA P-256, встроенный |
187 мкс |
191 мкс |
64 Б |
64 Б |
RSA-2048, встроенный |
505 мкс |
16 мкс |
256 Б |
294 Б |
Ed25519, BouncyCastle |
36 мкс |
77 мкс |
64 Б |
32 Б |
Ed25519, NSec + libsodium |
34 мкс |
91 мкс |
64 Б |
32 Б |
Смотрим на колонку «проверка» — и видим, что выбранный мной вариант ECDSA P-256 проиграл обоим. Ed25519 проверяет в 2,5 раза быстрее, RSA-2048 — в двенадцать раз быстрее, потому что проверка у RSA это возведение в маленькую открытую экспоненту. Подписывает RSA, наоборот, медленнее, но подпись происходит в моем случае один раз на лицензию, по этому на эту разницу в общем то можно и не смотреть.
И тут же цифра, которая ставит всю таблицу на место. Первая криптографическая операция в свежем процессе (создание ключа и подпись) занимает 6,81 мс: в тридцать пять раз дольше «горячей» проверки. То есть всё, что я могу выиграть выбором алгоритма, тонет в разовой инициализации при запуске. Разница между 191 и 77 микросекундами — это разница между «незаметно» и «незаметно».
Почему выбор решала длина строки, а не микросекунды
А теперь то, что и определило выбор способа кодировки. Ключ живёт в письме, его копируют, а иногда перепечатывают руками. Кодирую в Base32, а не в более компактный Base64, намеренно: в алфавите Base32 нет строчных букв и нет пар-двойников вроде 0 и O, 1 и l, так что ключ можно продиктовать по телефону и не получить в ответ: «не подходит». Считаем: 39 байт данных плюс подпись, группы по пять символов через дефис.
Алгоритм |
Подпись |
Всего байт |
Base32 |
Готовый ключ |
|---|---|---|---|---|
ECDSA P-256 или Ed25519 |
64 Б |
103 |
165 |
202 символа |
RSA-2048 |
256 Б |
295 |
472 |
571 символ |
RSA-3072 |
384 Б |
423 |
677 |
817 символов |
Пятьсот семьдесят один символ — это уже не лицензионный ключ, это простыня на пол-экрана! Значит, RSA отпадает не по производительности, по которой он как раз хорош, а по внешнему виду письма. Остаются два варианта с подписью в 64 байта: тот, которого в рантайме нет, и тот, который есть.
Сразу оговорюсь, чтобы не выдавать нужду за добродетель: 202 символа — это тоже много. Красивого ключа вида ABCD-1234-EFGH здесь не будет ни при каком выборе алгоритма и вот почему. В оффлайновой схеме ключ обязан нести в себе всё: и данные лицензии, и подпись, которой эти данные скреплены, попросту программе больше неоткуда взять правду, у неё нет ни базы, ни сети. Подпись меньше 64 байт при вменяемой стойкости не бывает, а значит нижняя граница длины задана математикой. Короткие ключи из рекламных буклетов живут в другой архитектуре: там строка — просто номер, который сервер сверяет со своей базой, и без интернета такой ключ не значит ничего. Я сознательно выбрал длинную строку и работу без сети вместо короткой строки и обязательного подключения. Так что 202 символа — это не «коротко», это наименее длинно из возможного.
Мелочь, которая ломает фиксированную длину
Уже по ходу дела выяснилась деталь, которую я по неопытности мог и пропустить. Подпись ECDSA записывают в двух видах, и один из них имеет плавающую длину: числа r и s кодируются как целые, а у них бывает разное количество значащих байт. Я подписал один и тот же блок данных пять тысяч раз.
Формат записи |
Что получилось |
Итог |
|---|---|---|
DER, он же Rfc3279DerSequence |
69 Б — 0,2 %, 70 Б — 25,3 %, 71 Б — 49,7 %, 72 Б — 24,9 % |
длина плавает |
IEEE P1363, он же |
64 Б — 100 % случаев |
длина фиксирована |
Для формата, где длина ключа известна заранее и разбор идёт по смещениям, годится только второй. К счастью, в .NET именно он стоит по умолчанию: SignData без указания формата возвращает P1363. Если бы я машинально взял DER, как принято в мире OpenSSL, ключ получился бы переменной длины, а разбор оброс проверками на ровном месте.
Похожая история с публичным ключом, который вшивается в приложение. В полном виде, с описанием алгоритма и кривой, он занимает 91 байт и превращается в 124 символа Base64 (выбор на более компактном варианте, т.к. читаемость уже не нужна, ключ вшит). Но кривая у меня одна и известна заранее, поэтому я храню только сами координаты точки: 64 байта, то есть 88 символов. Мелочь, а константа в исходнике выглядит человечнее. Кстати, именно эти 88 символов однажды вставили в окно активации вместо лицензионного ключа — с тех пор программа отдельно объясняет, что публичный ключ и ключ лицензии это разные вещи.
Чем пришлось “заплатить”
Было бы нечестно закончить на том, какой я молодец. У выбранного варианта есть недостаток, и он не косметический.
ECDSA зависит от качества случайного числа. При каждой подписи генерируется одноразовое значение, и если оно повторится для двух разных сообщений или окажется предсказуемым, приватный ключ вычисляется из этих подписей. На этом в своё время сгорели вполне серьёзные системы. У Ed25519 такой проблемы нет по построению: там значение выводится детерминированно из ключа и сообщения по RFC 8032. Мой аргумент в свою защиту скромный: подписываю я на одной офлайн-машине штатным генератором операционной системы, а не на устройствах пользователей, так что поверхность у этого риска маленькая. Но риск есть, и делать вид, что его нет, я не буду.
Побочный эффект той же недетерминированности проще: если перевыпустить одну и ту же лицензию дважды, строки ключа получатся разные. Мелочь, но реестр выданных ключей это должен учитывать, иначе однажды удивишься.
И третье, о чём меня наверняка спросят в комментариях, так что скажу сам и сразу: к кривой P-256 у части криптографов есть вопросы по происхождению её констант, и Ed25519 в этом смысле репутационно чище. Для моей модели угроз — «подделать лицензию недорогой десктопной программы» — этой разницей можно пренебречь. Для чего-то серьёзнее я бы взвешивал решения заново.
Что в итоге
Я остался на ECDSA P-256 с SHA-256: подпись 64 байта, ключ 202 символа, проверка 0,19 мс, ноль сторонних зависимостей, кроссплатформенно из коробки — сервер, который у меня подписывает служебные подтверждения, работает на Linux тем же кодом. Пять вещей, которые я из этого вынес.
1. «Модно» и «доступно» — разные списки. Ed25519 стоит везде, где вы посмотрите, кроме стандартной библиотеки той платформы, на которой вы пишете. Проверять надо не общественное мнение, а свою сборку.
2. Критерий выбора диктует форма продукта, а не бенчмарк. У меня всё решила длина строки в письме: 202 символа против 571. Скорость подписи, вокруг которой обычно ломают копья, не повлияла ни на что.
3. Зависимость живёт своим графиком. Четыре мегабайта чужого кода — это не только четыре мегабайта: это ещё и чужие решения о том, какие версии платформы поддерживать.
4. Мерить, а не вспоминать. У меня были записаны замеры полугодовой давности, но проект, где они делались, я удалил. Пришлось перемерить всё заново — и одно число из старых записей оказалось не тем, чем я его помнил.
5. Проигрыш в бенчмарке — не повод менять решение. Мой алгоритм медленнее обоих конкурентов на проверке. На фоне 6,81 мс разовой инициализации это не значит ничего.
Спасибо, если дочитали.
Если у вас в проекте лежит криптография, выбранная «по умолчанию, все же так делают», — самое время открыть и проверить, что именно вы выбрали и почему.
Комментарии (10)

erebmaethor
28.07.2026 02:31А как реализована ротация ключей и защита от ввода одного ключа (J3QQ4-...) в несколько инстансов софтины?

BborlasS Автор
28.07.2026 02:31Сразу спойлер: эта тема по хорошему часть следующей статьи "продолжения", так как первая, текущая, описывала анализ и исследовательскую часть в варианте оффлайн, а вторая, ту которую я в итоге и допилил, уже построена на онлайн активации с участием сервера.
Коротко ответить к сожелению наверное не выйдет.Ответ разделю условно на две части.
Ротация. Первый байт полезной нагрузки - номер ключа подписи (keyId). В приложении вшит не один публичный ключ, а словарь «номер → ключ». Смена выглядит так: генерирую новую пару, добавляю в словарь строку с новым номером, новые лицензии подписываю ею. Ранее выданные продолжают проверяться старым публичным ключом — ротация не должна "наказывать" тех, кто уже заплатил.
Если утёк приватный ключ, одной ротации мало: вор печатает валидные лицензии на старом. План на этот случай - релиз с новым keyId, старый помечается устаревшим, и проверка для него переводится в режим «доверяем только номерам лицензий из белого списка». Это работает потому, что у каждой лицензии есть собственный номер (4 байта в payload) и локальный реестр выданных: ключ, напечатанный вором, будет с номером, которого в реестре нет. Есть конечно и ограничение - и новый keyId, и список отзыва доезжают только до тех, кто обновился. Автообновление есть, но мгновенным это не делает.
Вторая пара ключей - у сервера активации, он подписывает короткоживущие тикеты, номер ключа едет прямо в тикете. Там ротация проще: тикет живёт 30 дней, обратная совместимость не нужна, достаточно, чтобы клиент при «подпись не сошлась» молча пошёл активироваться заново. Ровно эту ветку я сначала и не написал, а ключ сервера пришлось менять внепланово. На тесте вылезло: клиент со старым тикетом получал невалидную подпись и тихо уезжал в бесплатный режим вместо переактивации, то есть при смене ключа платный уровень погас бы разом у всех.
Два урока оттуда:
ротация не считается сделанной, пока не прогнал клиента, у которого на руках СТАРЫЙ артефакт;
порядок проверки «сначала ключ, потом тикет» пришлось перевернуть - негодная доверенность не должна мешать активации.
Один ключ в нескольких инстансах. Сама подписанная строка этого не умеет в принципе: подпись доказывает подлинность, а не уникальность. Счётчик активаций внутрь ключа не положить - payload скреплён подписью, а переподписать его на машине пользователя нечем, отдельный локальный счётчик рядом обнуляется копированием файла.
Единственное место, где такой счётчик может жить - это сервер.
Поэтому поверх офлайновой подписи стоит онлайн активация:
клиент шлёт ключ и обезличенный отпечаток машины — HMAC-SHA256 от MachineGuid и серийника системного тома, первые 16 байт;
сервер занимает слот (в рознице seats = 1) и возвращает подписанный СВОИМ ключом тикет: номер лицензии, хэш ключа, отпечаток устройства, слот, срок 30 дней, nonce клиента;
дальше программа работает офлайн: проверяет подпись сервера, пересчитывает отпечаток, смотрит срок. За подтверждением ходит не чаще раза в сутки, без сети живёт до 30 дней.
Второй экземпляр того же ключа получает отказ SEAT_TAKEN. Скопировать другу ключ вместе с тикетом тоже не помогает: доверенность выписана на чужое железо, отпечаток не сойдётся, а новую сервер не выдаст - слот занят. Переустановка Windows, поломка, переезд решаются не криптографией, а кнопкой «Освободить и активировать здесь» (доказательство владения - предъявить полный ключ), не чаще трёх раз в 30 дней, чтобы это не превратилось в «пользуемся вдвоём по очереди».
В базе при этом ни ключей, ни адресов в открытом виде: хэш ключа, хэш почты с маской, хэш отпечатка - утечка базы не даёт ни одной работающей лицензии.
И сразу предел всего происходящего, чтобы не создавать ложного впечатления: всё это защищает от копирования ключа, но не от патча бинарника. С ним конечно все обстоит куда сложнее.

fedorro
28.07.2026 02:31Какой-то оверкил для недорогой десктопной программы.
подпись перепроверяется в каждой точке, где платная функция реально исполняется
Если вызовом функции - то это патчится в одном месте, если как-то инлайнится - то тоже не на много сложнее, чем в одном месте поправить, тем более ИИ может всё это эвтоматизировать.

BborlasS Автор
28.07.2026 02:31Согласен с вами, я в статье сформулировал слишком широко. Перепроверки спасают от класса «правка живого значения»: единый флаг «оплачено» это поле, которое живёт весь сеанс, его находят поиском в памяти и переключают, не пересобирая сборку и не читая код. Но это время, желание сломать и навыки с знаниями. Когда решение нигде не залипает, переключать нечего.
От правки самой сборки они не спасают - тут вы правы. Гейт это один метод, и патчится он один раз, сколько бы точек его ни звали. Мне наверное конечно стоит подправить свою формулировку: не «защищает от патча», а «поднимает атаку на ступеньку: с правки значения в памяти до правки кода». Ступенька, не стена, в статье стоило написать именно так.
Вторую причину я в текст не вынес. У платных функций в приложении есть путь запуска без интерфейса: фоновая очистка стартует из Планировщика с ключом --scheduled, без окна и кнопок. Проверка, живущая на кнопке, обходится штатно - не злоумышленником, а самой программой. Поэтому права сверяются там, где действие исполняется.
Про оверкил: крипта здесь стоила пары сотен строк на встроенной библиотеке и 0,19 мс на проверку. Настоящим оверкилом было бы городить обфускацию до первых реальных продаж - этот шаг я как раз отложил.
Про ИИ, который всё автоматизирует - согласен. Гонку «сделать невзламываемо» недорогая десктопная программа выигрывать не должна и не может. Задача скромнее: чтобы честный путь был проще нечестного для обычного человека.

Granulex
28.07.2026 02:31Про 571-символьную "простыню" от RSA – прямо больно и знакомо. Есть ирония уровнем выше: вся эта аккуратная криптография честно защищает от того, чего никто не делает. Ключ никто не станет подделывать – его просто купят один раз и выложат на форум, а без сервера программа будет с гордостью проверять валиднейшую подпись на тысяче чужих машин. Математика подписи безупречна ровно до того момента, как один довольный пользователь нажмёт Ctrl+C.

BborlasS Автор
28.07.2026 02:31Здесь аналогичная тема ложащаяся в планируемое продолжение, где я бы раскрыл онлайн активацию и механизмы контроля, в текущей статье был условный первый этап, через который я сознательно решил пройти, так как любую тему пытаюсь пройти от "фундамента". Данные задачи я ставлю перед собой и разбираю в первую очередь для самообучения, так проще когда есть прикладная задача.
Согласен с вами по фактам и не согласен с выводом — попробую объяснить свою позицию.
Да, подпись не защищает от Ctrl+C, и не должна: её работа - доказать, что документ выписал я и что байты не переписали по дороге. «Сколько человек пользуется одним документом» это вопрос учёта, и в оффлайновой схеме, согласен, он не решается никак: счётчик внутрь ключа не положить (payload скреплён подписью, переподписать на машине пользователя нечем), а счётчик в файле рядом обнуляется копированием этого файла.
Причём описанный сценарий у меня не гипотетический. Это ровно первая редакция проекта: чистый офлайн, ключ из письма, никакого сервера. Я её зафиксировал и продолжил развивать примерно по вашим же аргументам, а статья описывает выбор алгоритма внутри слоя «транспорт прав», а не всю защиту целиком. J3QQ4-… из соседнего комментария жив как раз потому, что в его эпоху отклонять было нечему.
Что стоит поверх: активация на сервере. Ключ плюс обезличенный отпечаток машины: сервер занимает единственный слот и выдаёт подписанную им доверенность на 30 дней, привязанную к этому отпечатку. Выложенный на форум ключ активируется у первого пришедшего, все остальные получают «слот занят». Копирование ключа вместе с доверенностью не помогает: она выписана на чужое железо.
А теперь ирония в обратную сторону, ради которой я и отвечаю. Как только появляется сервер, криптография не становится лишней - она становится сильно актуальнее. Онлайн проверка без подписанных ответов ломается за десять минут: строка в hosts, свой «сервер активации» на localhost, свой сертификат и программа радостно слышат «лицензия подтверждена». HTTPS здесь не спасает: сертификат для собственного локального сервера пользователь на своей машине выпишет сам при должных знаниях и желании. Спасает то, что ответ подписан отдельным серверным ключом, публичная часть которого вшита в программу, плюс nonce от клиента, чтобы нельзя было подсунуть записанный вчера ответ. Подделать это - значит подделать подпись, то есть ровно та задача, про которую статья.
И вторая работа для той же подписи: она позволяет не ходить в сеть. Доверенность самоподписана сервером, поэтому программа спокойно живёт офлайн до 30 дней вместо безуспешного стука на сервер при каждом запуске.
Против: «вырезал проверку из бинарника» не работает ни подпись, ни сервер - только удорожание взлома, и я не топлю про 100% защиту. Задача этого слоя скромнее: закрыть массовый бытовой канал «скинь ключик», а он и составляет практически все потери у продуктов такого размера.
vobinoy987
200 символов диктовать по телефону)
Vest
Зачем, если есть SMS?
BborlasS Автор
Если речь про активацию в духе Windows XP: «позвоните, продиктуйте номер установки, получите код» то это не альтернатива подписи, а тот же самый механизм, только с человеком вместо интернета. Короткий код подтверждения в такой схеме ровно криптографический ответ сервера, просто усечённый до длины, которую можно продиктовать голосом; проверяет его всё равно программа на машине пользователя, всё той же математикой.
Есть и практическая сторона. Мой ключ самодостаточен: в нём лежат данные лицензии и подпись, потому что программе больше неоткуда взять правду: ни базы, ни сети. 202 символа в SMS не влезают (70 знаков кириллицей, 160 латиницей), а диктовать их по телефону это пытка для обеих сторон )). Короткими коды у Microsoft были не от хорошей жизни и не от лучшей криптографии: там строка, не документ, а челлендж к их базе, то есть решение принимает сервер, а телефон просто канал связи. Я же специально делал вариант, который работает без канала связи вообще.
Плюс два соображения, не технических. SMS - это платный шлюз, который должен жить круглосуточно и не падать, иначе покупатель не может включить то, за что заплатил. И это номер телефона, то есть персональные данные: у меня в базе сейчас хэш ключа и хэш почты с маской, ни одного телефона, и мне так спокойнее и юридически, и по-человечески.
Так что SMS не убирает криптографию, а добавляет к ней зависимость и персональные данные.
BborlasS Автор
Фигура речи)) Конечно столько никто диктовать не будет ))