Декодирование Base64 в C# (CSharp): полное руководство
Вы узнаёте её мгновенно: река букв и цифр, изредка + или /, а может быть, ещё пара =, торчащих в конце. Где-то между ответом API, вложением в письме, конфигурационным файлом и JWT кто-то упаковал двоичные данные в текст, и теперь ваша задача - распечатать его. Это сторона декодирования Base64 в C#, и первая хорошая новость в том, что вам нужна только сама платформа. Декодер живёт в пространстве имён System уже более двадцати лет, и каждая современная среда выполнения .NET по-прежнему поставляется с ним - с большим числом опций и лучшей производительностью, чем у оригинала.
Короткое повторение, потому что домашняя страница этого сайта полностью объясняет формат: четыре символа из 64-буквенного алфавита несут три байта данных, а один или два = в хвосте помечают оставшиеся байты. Декодирование проворачивает эту сделку в обратную сторону, поэтому результат примерно на три четверти меньше входа. Раз картина проблемы ясна, откроем-ка наши пакеты.
Семейство декодеров: знай свои варианты
Прежде чем перейти к первому примеру, вот целое семейство API декодирования, к которым можно обратиться, и ситуация, под которую каждый из них создан. Всё, что перечислено здесь, является частью самой среды выполнения .NET, кроме URL-безопасного класса на старых платформах - тот приехал на маленьком пакете NuGet:
| API | Доступен с | Для чего |
|---|---|---|
Convert.FromBase64String(string) |
.NET Framework 1.1 (2003) | Классика. На входе одна строка, на выходе свежий byte[]. Пропускает обычные пробельные символы, на всём остальном бросает исключение. |
Convert.FromBase64CharArray(char[], int, int) |
.NET Framework 1.1 (2003) | То же самое декодирование, но чтение идёт из среза символьного буфера, который у вас уже есть. |
Convert.TryFromBase64String, Convert.TryFromBase64Chars |
.NET Core 2.1 (2018) | Булево значение вместо исключений, запись в спан, который вы предоставляете. Дружелюбный страж для недоверенного ввода. |
System.Buffers.Text.Base64 |
.NET Core 2.1 (2018) | Строгие API на спанах: коды состояния вместо исключений, декодирование на месте и предварительная проверка через IsValid. |
System.Buffers.Text.Base64Url |
.NET 9 (2024) | URL-безопасный алфавит (- и _ вместо + и /), с заполнением или без. На .NET Framework 4.6.2+ и .NET Standard 2.0: пакет NuGet Microsoft.Bcl.Memory. |
FromBase64Transform + CryptoStream |
.NET Framework 1.1 (2003) | Потоковое декодирование: файл в файл, сеть на диск, кусок за куском, без загрузки всей нагрузки целиком. |
Если ваш проект нацелен на версию .NET 2018 года или новее, первые четыре строки уже в комплекте. Base64Url требует .NET 9 или новее, либо пакет Microsoft.Bcl.Memory на чём-то более старом. И взгляд вперёд: библиотеки .NET 11, на момент написания находящиеся в превью, с общим выпуском, которого ждут в конце 2026 года, добавляют существующим типам дополнительные удобные API и перегрузки Base64, так что семейство продолжает расти. Больше ничего в этой статье не требует пакетов.
Рабочая лошадка: Convert.FromBase64String
Девяносто процентов жизни декодирования в C# - это один вызов. Передайте ему строку, и он вернёт точные байты, которые были в ней упакованы:
using System;
using System.Text;
string packed = "TWFu";
byte[] bytes = Convert.FromBase64String(packed);
string text = Encoding.UTF8.GetString(bytes);
Console.WriteLine(text);
// Man
Три детали стоит запомнить. Первая: возвращаемое значение - это байты, а не текст: это byte[], декодер байтовый от и до, и именно этого вы хотите, потому что нагрузка может быть фразой, PNG, сертификатом или хэшем, и ни один из них не следует считать особым. Прыжок от байтов обратно к читаемому тексту - это отдельный, осознанный шаг через Encoding, и именно в этом шаге живут решения о кодировке (об этом ниже). Вторая: декодер выделяет свежий массив при каждом вызове, по размеру под длину декодированных данных, поэтому он никогда не отдаёт вам буфер с запасом. Третья: договорённость маленькая и честная: пустая строка декодируется в пустой массив, ссылка null бросает ArgumentNullException, а всё, что не является валидным Base64, бросает FormatException. Всё остальное - развитие этих трёх правил.
Что он прощает и что отвергает
Вот где у декодера C# начинается характер, и характер выразительный. Он щедр ровно в одной вещи - пробельных символах - и безжалостен ко всему остальному. Декодер пропускает ровно четыре символа, где бы они ни встречались в строке: пробел (U+0020), табуляция (U+0009), перевод строки (U+000A) и возврат каретки (U+000D). Эта политика - осознанное уважение к электронной почте, где нагрузки Base64 приходят, обёрнутые в строки по 76 символов, и это значит, что обёрнутое по MIME вложение декодируется без какой-либо предобработки. Всё, что вне 64-буквенного алфавита, всё, что ломает правила длины, и всё, у чего заполнение стоит не там, получает исключение. Смотрите, как тот же декодер работает с несколькими разными входами:
| Вход | Результат |
|---|---|
"TWFu" |
Декодируется в Man (3 байта). |
"TWF\nu" (перевод строки посередине) |
Декодируется в Man. Пробельные символы невидимы для декодера. |
"TWFu\u00A0" (неразрывный пробел в конце) |
FormatException. Пропускаются только четыре пробельных символа из списка выше; NBSP к ним не относится. |
"TWE" (длина 3, не кратно 4) |
FormatException. Длина нагрузки, если не считать пробельные символы, должна быть кратна 4. |
"TWFu=" (лишнее заполнение после данных) |
FormatException. Не более двух символов заполнения, и только в самом конце. |
"-_88" (URL-безопасный алфавит) |
FormatException. Стандартный декодер знает только 64 символа стандартного алфавита. |
null |
ArgumentNullException: Значение не может быть null. (Параметр 's') |
Ещё один нюанс, который стоит запомнить: каждый формальный проступок наказывается одним и тем же сообщением об ошибке: Входные данные не являются допустимой Base-64 строкой, так как содержат символ, не входящий в Base-64, более двух символов заполнения или недопустимый символ среди символов заполнения. Сообщение перечисляет все три возможные причины, но не говорит, о какую из них вы споткнулись, и не подсказывает, где. Если вы отлаживаете падающую нагрузку, посчитайте символы, проверьте алфавит и проверьте заполнение - в таком порядке.
Декодирование без исключений: Try API
Управление потоком через исключения - легитимный паттерн, но для массового или недоверенного ввода семейство Try - гражданин получше. Оно появилось в .NET Core 2.1 и бывает в двух видах: одно читает из строки, другое - из символьного спана. Оба пишут в буфер, который вы предоставляете, и сообщают, сколько в нём заняли:
using System;
using System.Text;
string payload = "TWFu"; // любая нагрузка, валидная или нет
Span<byte> buffer = stackalloc byte[4096];
if (Convert.TryFromBase64String(payload, buffer, out int written))
{
string text = Encoding.UTF8.GetString(buffer[..written]);
Console.WriteLine(text);
}
else
{
Console.WriteLine("Not a valid Base64 payload.");
}
Два поведения делают варианты Try похожими на другое создание. Неверный ввод возвращает false вместо того, чтобы бросать исключение, поэтому поток битых нагрузок обходится вам одной веткой, а не исключением. Одно предупреждение: null на входе не входит в договорённость - оно бросает ArgumentNullException - так что страж Try закрывает сломанные нагрузки, а для значения, которое может отсутствовать, сначала всё равно нужна отдельная проверка на null. Метод-сосед Convert.TryFromBase64Chars делает ту же работу из ReadOnlySpan<char>, и это удобно, когда нагрузка живёт в большом символьном буфере и вы не хотите сначала вырезать подстроку. Выделяйте выходной буфер с запасом: декодированная длина в худшем случае равна трём четвертям длины входа (без учёта пробельных символов), а out-параметр written говорит вам точно, сколько вышло.
Декодирование на спанах: System.Buffers.Text.Base64
Когда вы считаете выделения памяти или хотите, чтобы декодер описывал свои сбои, а не бросал исключения, класс System.Buffers.Text.Base64 - именно тот инструмент. Он живёт статическим классом в стандартной библиотеке с .NET Core 2.1 и работает со спанами, а не с управляемыми массивами. Его метод декодирования возвращает значение OperationStatus с четырьмя настроениями: Done (успех), DestinationTooSmall (ваш буфер был слишком мал), NeedMoreData (вход пока не кратен 4, продолжаем читать) и InvalidData (это не Base64). Последний булев параметр, isFinalBlock, как раз и отличает эти два состояния: он говорит декодеру, ждёт ли ещё входных данных. Вот однократная форма, размер подогнан собственным помощником класса:
using System.Buffers;
using System.Buffers.Text;
using System.Text;
string payload = "TWFu";
byte[] input = Encoding.ASCII.GetBytes(payload);
byte[] output = new byte[Base64.GetMaxDecodedFromUtf8Length(input.Length)];
OperationStatus status = Base64.DecodeFromUtf8(input, output,
out int consumed, out int written, isFinalBlock: true);
if (status == OperationStatus.Done)
{
Console.WriteLine(Encoding.UTF8.GetString(output.AsSpan(0, written)));
// Man
}
Два других члена этого класса заслуживают абзаца. Первый - IsValid, который проверяет нагрузку, не декодируя её. Он бывает в виде спана байтов и спана символов, а одна из перегрузок сообщает декодированную длину вместе с вердиктом, так что по одной проверке можно подобрать размер буфера:
using System.Buffers.Text;
string payload = "TWFu";
if (Base64.IsValid(payload, out int decodedLength))
{
Console.WriteLine("Valid, decodes to " + decodedLength + " bytes.");
// Valid, decodes to 3 bytes.
}
else
{
Console.WriteLine("Rejecting payload before allocating anything.");
}
Второй - DecodeFromUtf8InPlace, для ситуации, где текст Base64 уже лежит в буфере, которым вы владеете, и вас не смущает его перезаписать. Декодирование сжимает данные, поэтому результат записывается в начало того же буфера, а метод сообщает, какой у него получился размер:
using System.Buffers;
using System.Buffers.Text;
using System.Text;
byte[] data = Encoding.ASCII.GetBytes("TWFu");
OperationStatus status = Base64.DecodeFromUtf8InPlace(data, out int written);
if (status == OperationStatus.Done)
{
Console.WriteLine(Encoding.ASCII.GetString(data, 0, written));
// Man, теперь живёт в первых трёх байтах того же буфера
}
Одно поведение, которое стоит держать в кармане: этот класс тоже пропускает четыре обычных пробельных символа (пробел, табуляция, перевод строки, возврат каретки), так что обёрнутая строками нагрузка декодируется так же хорошо. Он строг в тех местах, где это важно: нагрузка, длина которой без пробельных символов не кратна четырём, при последнем блоке становится InvalidData, а символы вне стандартного алфавита отклоняются сразу. Молчаливой чистки нет нигде в этом классе.
URL-безопасный Base64: класс Base64Url
Для тех же 64 значений существует второй алфавит, и в веб-работе на C# вы будете встречать его постоянно. В стандартном алфавите значения 62 и 63 - это + и /, два символа, которые доставляют неприятности в URL: + в строке запроса регулярно декодируется как пробел, а / и = каждому требуется процентная кодировка. RFC 4648, раздел 5, чинит это, подставляя - и _, которые не несут особого смысла ни в одном URL-контексте, и делает хвостовое заполнение = необязательным. Результат называется base64url, и это алфавит JWT, API-токенов, идентификаторов загруженных файлов и великого множества URL (11-символьные идентификаторы видео YouTube - это base64url без заполнения).
С .NET 9 стандартная библиотека поставляется с выделенным для него классом: System.Buffers.Text.Base64Url. Это URL-безопасный двойник класса Base64 со своими помощниками декодирования, проверки и расчёта длины:
using System.Buffers.Text;
using System.Text;
string token = "-__8";
byte[] bytes = Base64Url.DecodeFromChars(token);
Console.WriteLine(BitConverter.ToString(bytes));
// FB-FF-FC
Обратите внимание, чего классический API с этим примером сделать бы не смог. Те же три байта в стандартном алфавите кодируются как +//8, и Convert.FromBase64String("+//8") работает, но Convert.FromBase64String("-__8") бросает исключение, потому что URL-безопасные символы вне его алфавита. А нагрузки base64url часто приходят без заполнения, что классический декодер тоже отклоняет, потому что требует полную четвёрку. Класс Base64Url нативно обрабатывает оба варианта этой проблемы: он декодирует TWE (три символа, без заполнения) в два байта Ma, и TWE= декодирует ничуть не хуже.
Если ваш проект работает на более старой среде выполнения, есть два практичных пути. На .NET Framework 4.6.2 и новее добавьте пакет NuGet Microsoft.Bcl.Memory, который Microsoft публикует специально для бэкопорта Base64Url (вместе с несколькими другими современными типами):
dotnet add package Microsoft.Bcl.Memory
Или вообще без какого-либо пакета: нормализуйте нагрузку перед тем, как отдать её классическому декодеру: поменяйте URL-безопасные символы обратно на их стандартных двойников и допишите недостающее заполнение. Этот маленький помощник - самый распространённый самодельный декодер base64url в коде на C#, и его стоит знать, потому что он работает на каждой среде выполнения начиная с .NET Framework 1.1:
using System;
using System.Text;
string segment = "TWE";
segment = segment.Replace('-', '+').Replace('_', '/');
segment += new string('=', (4 - segment.Length % 4) % 4);
byte[] bytes = Convert.FromBase64String(segment);
Console.WriteLine(Encoding.ASCII.GetString(bytes));
// Ma
Формула (4 - length % 4) % 4 - это и есть вся арифметика заполнения: она добавляет ноль, один или два знака =, чтобы длина уложилась в кратно четырёх, а внешний остаток от деления не даёт уже заполненному входу подхватить лишние.
От байтов к словам: текст, Unicode и кодировки
Декодирование отдаёт вам байты, а байты - вещь совершенно нейтральная. Они становятся «текстом» только тогда, когда вы выбираете кодировку, в которой их читать, и этот выбор за вами, потому что Base64 не несёт никакой информации о том, какую кодировку использовал первоначальный автор. На практике это значит: считайте UTF-8, если у вас нет причин делать иначе, и указывайте это явно в коде, потому что явный вызов Encoding.UTF8 - разница между программой, которая работает правильно по случайности, и программой, которая правильна по замыслу:
using System;
using System.Text;
string original = "h\u00e9llo \u4e16\u754c";
byte[] utf8 = Encoding.UTF8.GetBytes(original);
string packed = Convert.ToBase64String(utf8);
byte[] decoded = Convert.FromBase64String(packed);
string restored = Encoding.UTF8.GetString(decoded);
Console.WriteLine(restored == original);
// True: h\u00e9llo \u4e16\u754c проходит туда-обратно без потерь
Тонкая ловушка - это то, что происходит, когда байты не являются валидным UTF-8, потому что нагрузка была на самом деле Latin-1, или двоичной, или просто повреждена. По умолчанию UTF-8 декодер .NET заменяет каждую некорректную последовательность на символ замены Unicode (U+FFFD) и идёт дальше. Ни исключений, ни предупреждений: данные просто пропадают, превращаясь в вопросики в вашей базе данных. Если вам нужно знать, когда это происходит, создайте кодировку со строгим обработчиком несовпадений - он превратит молчаливую замену в громкий DecoderFallbackException:
using System.Text;
byte[] bytes = Convert.FromBase64String("//4="); // байты FF FE, некорректный UTF-8
Encoding strictUtf8 = Encoding.GetEncoding(
"utf-8",
new EncoderExceptionFallback(),
new DecoderExceptionFallback());
string text = strictUtf8.GetString(bytes);
// Бросает DecoderFallbackException, потому что FF FE не является последовательностью UTF-8
Для нагрузок, где важнее выжить, чем упасть, мягче вариант с обработчиками замены, и текст замены выбираете вы сами:
using System.Text;
byte[] bytes = Convert.FromBase64String("//4="); // байты FF FE, некорректный UTF-8
Encoding forgivingUtf8 = Encoding.GetEncoding(
"utf-8",
EncoderFallback.ReplacementFallback,
new DecoderReplacementFallback("[bad]"));
string text = forgivingUtf8.GetString(bytes);
Console.WriteLine(text);
// [bad][bad] вместо молчаливой замены на U+FFFD
Ещё один исторический урок, специфичный для C#: Encoding.Default означает разные вещи в разных средах выполнения. На .NET Framework под Windows это ANSI-кодировка системы (часто Windows-1252), а на .NET (Core) это UTF-8 без BOM. Поэтому код, который гоняет нагрузку туда-обратно через Encoding.Default, может дать разные байты на машине 2010 года и на машине 2025 года, а Base64 охотно закодирует любой набор, который ему передадут. Если вы когда-нибудь увидите декодированную строку, полную кракозябр с чужими акцентами, Encoding.Default - первое место, которое стоит осмотреть.
Файлы и двоичные нагрузки
Файлы - самая прямолинейная цель декодирования, потому что вопроса о кодировке здесь нет совсем: байты, которые вы декодируете, и есть файл, байт за байтом, нули и всё такое. Паттерн - два вызова и файл, и он встречается везде: от загрузки изображений до инструментов резервного копирования:
using System.IO;
string b64 = File.ReadAllText("payload.b64");
byte[] original = Convert.FromBase64String(b64);
File.WriteAllBytes("restored.bin", original);
Console.WriteLine("Restored " + original.Length + " bytes.");
Две практические заметки. Если файл может содержать пробельные символы или переводы строк (а будучи текстовым файлом, он почти наверняка содержит), классический декодер разберётся с этим бесплатно, как вы видели раньше. А если нагрузка большая, вообще не ходите через строку: пропустите шаг «файл в строку» и декодируйте прямо из потока, об этом следующий раздел. Для декодированной нагрузки, которая является текстом и кодировку которой вам, случается, известна, пример с файлом - целое решение, и шаг Encoding.UTF8.GetString из раздела о кодировках как раз вставляется между декодированием и использованием.
Декодирование из потока: FromBase64Transform
Методы Convert рассчитаны на нагрузки, которые помещаются в строку, и официальная документация говорит об этом буквально: для потоковых данных используйте классы преобразования. FromBase64Transform входит в System.Security.Cryptography с .NET Framework 1.1 (2003) и подключается к CryptoStream - универсальному каналу платформы для преобразования данных по мере их потока. Всё декодирование «файл в файл» - это настройка из четырёх строк:
using System.IO;
using System.Security.Cryptography;
using FileStream source = File.OpenRead("payload.b64");
using FromBase64Transform transform =
new FromBase64Transform(FromBase64TransformMode.IgnoreWhiteSpaces);
using CryptoStream reader = new CryptoStream(source, transform, CryptoStreamMode.Read);
using FileStream target = File.Create("payload.bin");
reader.CopyTo(target);
Console.WriteLine("Done, " + target.Length + " bytes written.");
Конструктор принимает режим, и два режима стоит знать по именам. IgnoreWhiteSpaces (режим по умолчанию, совпадающий с политикой пробельных символов классического декодера) пропускает четыре обычных пробельных символа, пока поток течёт, - это то, что вам нужно для обёрнутых почтовых нагрузок или нагрузок, усеянных переводами строк. DoNotIgnoreWhiteSpaces строг: первый встреченный символ, не входящий в алфавит, бросает FormatException, и это то, что вам нужно, когда лишний пробел в нагрузке должен быть багом, а не пожиманием плечами. Под капотом преобразование обрабатывает вход группами по четыре символа и отдаёт три байта, которые производит каждая группа, а TransformFinalBlock разбирается с хвостом. Вы редко вызываете эти методы сами, потому что CryptoStream делает это за вас, но факт про четвёрки важен: если вы когда-нибудь будете подкармливать преобразование вручную, кормите его кратно четырём, иначе последняя неполная группа окажется в финальном блоке.
JWT: три сегмента, одна точка
JSON Web Token - самая проходимая нагрузка base64url в веб-разработке на C#, и её форма обманчиво проста: три сегмента, разделённые точками. Первый - закодированная шапка, второй - закодированная нагрузка (также известная как claims), третий - подпись. Каждый из первых двух - это base64url UTF-8 JSON-документа, без заполнения, согласно спецификации JWS. Разделить и декодировать - это две строки C#:
using System;
using System.Buffers.Text;
using System.Text;
using System.Text.Json;
string jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsIm5hbWUiOiJBZGEifQ.c2lnbmF0dXJl";
string[] parts = jwt.Split('.');
string headerJson = Encoding.UTF8.GetString(Base64Url.DecodeFromChars(parts[0]));
string payloadJson = Encoding.UTF8.GetString(Base64Url.DecodeFromChars(parts[1]));
using JsonDocument doc = JsonDocument.Parse(payloadJson);
Console.WriteLine(doc.RootElement.GetProperty("name").GetString());
// Ada
На средах выполнения до .NET 9 та же работа идёт через помощник нормализации из раздела о URL-безопасном: поменяйте - и _ обратно на + и /, дополните сегмент до кратного четырём и декодируйте через Convert.FromBase64String. Оба подхода дают один и тот же JSON; выбирайте тот, что соответствует вашей целевой платформе.
Одна граница, которую важно держать острой: декодировать JWT - не значит проверять JWT. Декодирование выше с удовольствием прочитает claims токена с мусорной подписью, потому что подпись - это отдельная криптографическая проверка по первым двум сегментам. Для боевой работы с токенами вообще не разбирайте вручную: пакет System.IdentityModel.Tokens.Jwt (из семейства Microsoft.IdentityModel) одним махом берёт на себя разбор, валидацию и срок действия, и его обработка base64url - это ровно тот алфавит, о котором рассказывает этот раздел. Декодируйте вручную для отладки и маленьких утилит; проверяйте библиотекой всё, что может дотянуться пользователь.
Data URI и встроенные изображения
Существует целый класс кода на C#, чья работа - принимать data: URI, потому что HTML, CSS и великое множество веб-API используют их, чтобы встраивать двоичный контент прямо в текст. Схема, стандартизированная RFC 2397, выглядит так: data:[mediatype][;base64],payload: всё до первой запятой - метаданные (MIME-тип и флаг ;base64), всё после - нагрузка. Когда флаг ;base64 присутствует, нагрузка - это строка Base64, и разбиение по запятой - весь разбор:
using System;
using System.Text;
string dataUri = "data:image/png;base64,iVBORw0KGgo=";
int comma = dataUri.IndexOf(',');
string mediaType = dataUri[..comma]; // data:image/png;base64
string b64 = dataUri[(comma + 1)..]; // iVBORw0KGgo=
byte[] imageBytes = Convert.FromBase64String(b64);
Console.WriteLine(imageBytes.Length);
// 8: байты сигнатуры PNG 89 50 4E 47 0D 0A 1A 0A
Префикс iVBORw0KGgo= в примере - это Base64-форма восьмбайтного магического числа PNG, и это полезный отпечаток: любой data URI настоящего PNG начинается именно так, так что это быстрая проверка здравым смыслом, когда вы разбираете недоверенный HTML. Две практические заметки для разработчиков на C#. Первая: класс Uri на .NET нативно понимает data URI: new Uri("data:text/plain;base64,TWFu") разбирается без проблем и сообщает Scheme == "data", так что если ваш код маршрутизирует по URI, data URI появятся в конвейере, и вам стоит решить, как их обрабатывать. Вторая: помните, что такое data URI на самом деле: полная копия файла, раздутая на треть, живущая внутри вашего документа. Для фавиконки 4 КБ это нормально, для логотипа 4 МБ - боль, поэтому если генерируете их вы (сторону кодирования покрывает статья о кодировании), определите размер изображения до того, как вы его закодируете.
HTTP: Basic auth и обмен по API
Base64 вплетён в HTTP хотя бы в одном месте, к которому вы прикоснётесь в любой API-работе: схема Basic auth. Клиент присылает Authorization: Basic со Base64-кодом username:password, склеенными двоеточием. На стороне сервера декодирование входящего заголовка, следовательно: отсечь префикс Basic , декодировать и разделить по первому двоеточию:
using System;
using System.Text;
string header = "Basic YWRhOnMzY3JldA==";
string encoded = header["Basic ".Length..].Trim();
string credentials = Encoding.UTF8.GetString(Convert.FromBase64String(encoded));
int colon = credentials.IndexOf(':');
string user = credentials[..colon];
string password = credentials[(colon + 1)..];
Console.WriteLine(user); // ada
Console.WriteLine(password); // s3cret
Шаг с UTF-8 важен больше, чем кажется: RFC 7617 на самом деле не закрепляет кодировку, оставляя значение по умолчанию неопределённым ради обратной совместимости и разрешая лишь рекомендательную подсказку UTF-8, но именно эту подсказку ожидает каждый современный сервер, так что имя пользователя с акцентированным символом даст другую (и правильную) строку байтов, чем то же имя, прочитанное как Latin-1. Декодирование в Basic-аутентификации - простая сторона этого паттерна; в ASP.NET Core вы обычно встретите его через обработчики аутентификации, а не через голые заголовки, но та же логика декодирования работает у них под капотом, и это ровно тот код, который вам нужен, когда вы пишете интеграционные тесты, имитирующие API-сервер. Зеркальная операция, сборка заголовка на стороне клиента, - одна строка со стороны кодирования, и в статье о кодировании ей достаётся полноценный пример.
Электронная почта: MIME и обёрнутые строками нагрузки
Электронная почта - место, где Base64 заработал свою репутацию, и по сей день это источник многих нагрузок, которые получают C#-сервисы. SMTP изначально был 7-битным протоколом, поэтому двоичные вложения не могут лететь голыми: спецификация MIME (RFC 2045) кодирует их в Base64 с заголовком Content-Transfer-Encoding: base64, обёртывает вывод на 76 символах и разделяет строки парами «возврат каретки - перевод строки». Тело настоящего вложения, следовательно, выглядит колонкой 76-символьных строк, и хорошая новость для C# в том, что классический декодер уже умеет это читать: поскольку он пропускает пробельные символы где угодно в строке, можно отдать ему всё обёрнутое тело, переводы строк и всё такое, и он декодирует его так, будто переводов строк никогда не было:
using System;
using System.Text;
string attachmentBody = "TWFu\r\nTWFu\r\nTWFu";
byte[] bytes = Convert.FromBase64String(attachmentBody);
Console.WriteLine(Encoding.ASCII.GetString(bytes));
// ManManMan
Для нагрузок, которые приходят через поток, а не через строку, FromBase64Transform в его режиме игнорирования пробельных символов - та же история в потоковом костюме. А когда нужно сделать больше, чем просто декодировать тело, когда нужно обойти структуру MIME, разобрать заголовки, обработать вложенные multipart-разделы или вытащить каждое вложение из настоящего файла .eml, ответ экосистемы на C# - пакет MimeKit: это стандартная MIME-библиотека для .NET, она сама разбирается с кодировками переноса содержимого Base64 и quoted-printable, и это тот инструмент, за который берутся, как только «просто декодируй тело» перестаёт описывать вашу проблему. Собственный класс MailMessage платформы декодирует для вас простые вложения, но его поддержка MIME намеренно скромна по современным меркам.
Сертификаты PEM
PEM - это бронированный формат мира TLS: тело Base64 между маркерами -----BEGIN CERTIFICATE----- и -----END CERTIFICATE-----, обёрнутое на 64 символах, как указано в RFC 7468. Разработчики на C# встречают его в виде файлов сертификатов, стоящих за каждым HTTPS-эндпоинтом, и история декодирования здесь лучше, чем вы могли бы ожидать, потому что с .NET 6 платформа разбирает PEM за вас, тело Base64 и всё остальное:
using System.IO;
using System.Security.Cryptography.X509Certificates;
string pem = File.ReadAllText("server.pem");
X509Certificate2 certificate = X509Certificate2.CreateFromPem(pem);
Console.WriteLine(certificate.Subject);
// CN=server.example.com
Ни одного ручного Base64 во всём этом: CreateFromPem находит маркеры, снимает обёртку, декодирует и отдаёт вам живой сертификат. (В семействе есть братья и для закрытых ключей, и для объединённой формы «сертификат плюс ключ», если ваша инфраструктура даёт вам именно их.) Если вы на более старой среде выполнения или вам нужны сырые байты DER, лежащие внутри брони, ручной вариант - это двухшаговое «сними и декодируй», и его стоит знать, потому что тот же паттерн работает для любого PEM-обёрнутого объекта:
using System;
using System.Text;
string pem = File.ReadAllText("server.pem");
string body = pem
.Replace("-----BEGIN CERTIFICATE-----", "")
.Replace("-----END CERTIFICATE-----", "")
.Replace("\r", "")
.Replace("\n", "");
byte[] der = Convert.FromBase64String(body);
Console.WriteLine(der.Length);
// длина DER-сертификата внутри обёртки
Ловушки в этом углу - всё про пробельные символы: файлы PEM несут CRLF-окончания строк от большинства инструментов для сертификатов, поэтому перед декодированием отрезайте и \r, и \n, а не только переводы строк. И не путайте тело сертификата с телом закрытого ключа, у которого другие маркеры и другое содержимое; от этого декодер вас не спасёт.
Конфигурация, переменные окружения и базы данных
Третий дом Base64 в C#-приложениях - это хранение: конфигурационные файлы, переменные окружения и столбцы баз данных. Паттерн везде один и тот же. Двоичное или секретное значение по пути в кодируется в строку, а по пути в обратном направлении декодируется в байты. Переменные окружения - самый наглядный пример, потому что они способны держать только текст:
using System;
using System.Text;
string? encoded = Environment.GetEnvironmentVariable("API_KEY_B64");
if (encoded == null)
{
throw new InvalidOperationException("Set the API_KEY_B64 environment variable first.");
}
byte[] keyBytes = Convert.FromBase64String(encoded);
string apiKey = Encoding.UTF8.GetString(keyBytes);
Console.WriteLine(apiKey.Length + " characters of API key, ready to use.");
В базе данных та же идея обычно выступает в виде свойства byte[], которое хочется сохранить в текстовом столбце ради переносимости, и у Entity Framework Core есть встроенный механизм ровно для этого: конвертер значений, который прозрачно прогоняет ваши функции кодирования и декодирования при каждом чтении и записи:
using Microsoft.EntityFrameworkCore;
modelBuilder.Entity<Avatar>()
.Property(a => a.ImageData)
.HasConversion(
v => Convert.ToBase64String(v),
v => Convert.FromBase64String(v));
Этот единственный конвертер - вся интеграция с базой данных: ImageData остаётся byte[] в вашем коде на C#, а база видит строку Base64. Две оговорки принадлежат этому разделу. Первая: столбец заданной ширины держит в виде закодированного текста примерно на треть меньше данных, чем в виде сырых двоичных, из-за налога «4 символа за 3 байта», так что если ширина фиксированная, размерируйте столбец под закодированную длину. Вторая, и это оговорка про безопасность: Base64 в конфигурационном файле - это удобство, чтобы держать значение в одной строке, а не защита этого значения. Любой, кто может прочитать конфигурационный файл, может декодировать ключ одной командой, поэтому настоящие секреты живут в хранилище секретов, а Base64 там - лишь транспортный формат.
Когда нагрузка большая
У декодирования Base64 есть приятное свойство, которого нет у кодирования: выход всегда меньше входа, примерно три четверти от него. Текстовая нагрузка в 10 мегабайт декодируется примерно в 7,5 мегабайта байтов, поэтому декодирование никогда не раздувает память так, как может раздуть кодирование. Арифметика, если вам нужно заранее подобрать размер буфера, сводится к одному из двух вызовов: Base64.GetMaxDecodedFromUtf8Length для строгого спан-класса или простое деление length / 4 * 3 для классического API, плюс поправка на пробельные символы, если вход обёрнут. (Помощник возвращает максимально возможную декодированную длину: реальная длина равна ей только тогда, когда в последней группе нет заполнения, и на один или два байта меньше, когда группа заканчивается одним или двумя символами заполнения.)
Когда нагрузка по-настоящему большая, правильный ход - это не больший буфер, а вообще никакой буфер: полностью пропустите строку и позвольте FromBase64Transform потоково декодировать от источника к цели, как показано в разделе о потоках. Единственное правило, которого стоит держаться, - выравнивание по четвёркам: поток Base64 можно резать только на кратных четырём символах (после того, как учтены пробельные символы), так что если вы будете подкармливать преобразование вручную, читайте кусками, кратными четырём, и позвольте TransformFinalBlock слить остаток. Для всего, что меньше сотен мегабайт, однократного декодирования достаточно быстро, так что это оптимизация, а не необходимость, но именно потоковая форма хорошо ведёт себя при ограничениях памяти - а это как раз те среды, где любят жить большие нагрузки.
Декодер в вашем терминале
В каждом языке есть приятный момент, когда 15-строчная консольная программа становится командным инструментом, и C#-декодер Base64 - хороший кандидат для этого, потому что чтение из стандартного ввода делает его готовой заменой для конвейеров оболочки. Вот весь инструмент: он читает нагрузку Base64 из конвейера (или из аргумента), декодирует её и записывает сырые байты в файл:
using System;
using System.IO;
using System.Text;
string input = args.Length > 0 ? File.ReadAllText(args[0]) : Console.In.ReadToEnd();
byte[] bytes = Convert.FromBase64String(input.Trim());
File.WriteAllBytes("output.bin", bytes);
Console.Error.WriteLine("Wrote " + bytes.Length + " bytes to output.bin.");
Соберите его один раз, и он будет сидеть рядом с собственной утилитой base64 оболочки в те дни, когда вам конкретно нужен декодер среды выполнения .NET: прогоните через него файл, склейте его с другими инструментами, и строгие правила валидации C# (терпимость к пробельным символам, строгость к алфавиту, строгость к заполнению) станут частью вашего конвейера. Trim() там выполняет тихую работу, подлавливая хвостовой перевод строки, который любят добавлять текстовые редакторы, хотя, честно говоря, декодер бы проигнорировал его в любом случае. Для URL-безопасных нагрузок, которые всё чаще мелькают в API-логах, тот же каркас с декодированием Base64Url из раздела о URL-безопасном - и есть всё изменение.
Скорость: чего ждать
Base64 в современном .NET быстрый, и с каждым разом становится быстрее. Реализации среды выполнения - и методов Convert, и классов System.Buffers.Text - оптимизированы с помощью SIMD-векторных инструкций там, где их поддерживает железо, и они обрабатывают множество символов за такт. На практике это значит, что мультимегабайтные нагрузки декодируются за однозначные или низкие двузначные миллисекунды на обычном настольном компьютере, и это достаточно быстро, чтобы декодирование Base64 было фактически бесплатным в любом приложении, которое вы напишете. Практический совет по производительности, следовательно, про форму вашего кода, а не про самого декодера. На горячих путях, где возможен кривой ввод, а исключения будут дороги, предпочитайте методы Try или возвращающие статус спан-методы. Повторно используйте буферы через API «на месте» и спаны, когда декодируете тысячи маленьких нагрузок в цикле, вместо того чтобы выделять свежий массив на каждый вызов. И никогда не декодируйте одну и ту же нагрузку дважды: один раз - это цена, а второе декодирование поля, которое вы уже декодировали, - чистая трата, которая в профилях появляется как таинственный второй пик Base64.
Безопасность: чего Base64 не делает
Самый важный факт о безопасности Base64 - тот, который новички чаще всего пропускают: это кодирование, а не шифрование. Строку Base64 может прочитать любой, любым инструментом, за долю секунды, и C# делает это чтение одной строкой, как продемонстрировала вся эта статья. У Base64 нет ключа, нет параметра алгоритма и нет уязвимости, которую можно эксплуатировать, потому что он никогда не пытался ничего скрывать: это транспортный формат, способ заставить двоичные данные пережить каналы, где есть только текст. Относитесь к нему соответственно. Никогда не кладите пароль, токен или секрет в конфигурационный файл, «защищённый» Base64, потому что эта защита - ровно один вызов Convert.FromBase64String от открытого вида. Если значение должно быть секретным, ему нужна настоящая защита (менеджер секретов, зашифрованное хранилище, как минимум, контроль доступа операционной системы), а Base64 - лишь форма, которую оно надевает в пути.
Вторая оговорка о безопасности - про ваш собственный путь декодирования. Каждая нагрузка, которую вы декодируете, - недоверенный ввод, пока не доказано обратное, и два режима сбоя, под которые стоит проектировать, - громкий (неверный ввод, на который классический API отвечает FormatException, который вы должны перехватить и превратить в 400, а не в 500) и тихий (валидный Base64, который декодируется в байты, не те, что вы ожидали: не UTF-8, не тот тип файла, который вы просили, или длиннее, чем вы закладывали). Проверяйте до того, как доверяйте: проверьте длину через IsValid или семейство Try до того, как выделять память, сверьте декодированные байты с ожидаемой сигнатурой (магия PNG, заголовок PKCS) до того, как отдать их парсеру изображений или сертификатов, и подбирайте размер буферов по закодированной длине до декодирования, а не после. Base64 декодирует всё, что оформлено правильно; решать, что «оформлено правильно» означает для вашего приложения, - ваша работа.
Ловушки, которые стоит знать, пока они не укусили
Это C#-специфичные ямы, которые постоянно высовываются в реальном коде, и у каждой из них есть конкретная причина в том, как работает платформа:
- Двоичное через строку. Строка
stringв C# - это последовательность юнитов кодировки UTF-16, и декодированный Base64 ею не является. В ту секунду, когда вы заталкиваете декодированные байты в строковую переменную (Console.WriteLineдекодированного PNG, строковое сложение с двоичными, JSON-библиотека, которая сериализует «текст»), что-то ниже по потоку это исказит. Держите декодированные двоичные вbyte[], пока они не дойдут до места, которое действительно хочет байты. - Раскол Encoding.Default. Код, который читает декодированные байты через
Encoding.Default, даёт разный текст на .NET Framework (ANSI-кодировка Windows) и на .NET (UTF-8). Одна и та же нагрузка, два разных результата, ни одного исключения. Закрепите кодировку явно. - Сегменты JWT и классический декодер. Если подать сырой сегмент JWT в
Convert.FromBase64String, он упадёт двумя способами сразу: символы-/_вне стандартного алфавита, а недостающее заполнение ломает правило длины. Сначала нормализуйте, либо используйтеBase64Url. - Пробелы, которые видны, и пробелы, которые невидимы. Декодер пропускает пробел, табуляцию, перевод строки и возврат каретки, и ничего больше. Неразрывный пробел, разделитель строк Unicode или вертикальная табуляция в нагрузке (всё это переживает копирование из некоторых веб-страниц) - это
FormatException, а не пожимание плечами. - Одно сообщение об ошибке на все преступления.
FormatExceptionот классического декодера не говорит, какое правило сломалось и где. Отлаживайте, проверяя длину, потом алфавит, потом заполнение - в таком порядке, либо переключитесь наTryFromBase64StringиIsValidради булева ответа. - Молчаливая замена UTF-8.
Encoding.UTF8.GetStringпревращает некорректные байтовые последовательности в U+FFFD без единого замечания. Если нагрузка может оказаться не валидным UTF-8, используйте строгий обработчик несовпадений из раздела о кодировках, иначе будете исследовать пропавшие данные через недели после того, как это случилось. - Разрез потока не в том месте. Поток Base64 можно резать только на кратных четырём символах. Если кусковать потоковое декодирование на любой другой границе, последняя неполная группа окажется в
TransformFinalBlock, где ей либо место, либо она ломает ваш учёт выравнивания. - Окончания строк в PEM. Файлы сертификатов несут CRLF. При ручной разборке брони отрезайте и
\r, и\n, иначе первая строка вашего «декодированного» DER - это возврат каретки в одежде байта данных. - Двойное кодирование. Если нагрузка была уже Base64, когда дошла до вас (конфиг, который прогнал Base64-строку через Base64, API, закодировавшее вывод другого кодировщика), одно декодирование даст вам ещё больше Base64, а не ваши данные. Круг замыкается только после того числа декодирований, сколько было кодирований, а сторона кодировщика этого бага - предмет статьи о кодировании.
Короткая история Base64 в C#
История Base64 в C# - это ещё и история взросления платформы .NET, и она тянется дольше, чем большинство ожидает:
- .NET Framework 1.1, апрель 2003. Прибывают
Convert.FromBase64Stringи его братья, и они несут тот дизайн, который и по сей день определяет API: строгий к алфавиту, щедрый к четырём пробельным символам, прямой в ошибках. Бóльшую часть следующих двух десятилетий именно этот метод - «тот самый» декодер Base64 в C#. - .NET 2.0, 2005. Перечисление
Base64FormattingOptionsвливается вConvert, принося переводы строк в стиле MIME на сторону кодирования (и терпимость к пробельным символам на стороне декодирования, где она уже тихо работает). - .NET Core 2.1, 2018. Эра спанов.
Convertполучает методыTryи спан-кодирование, а новый классSystem.Buffers.Text.Base64прибывает со своим договоромOperationStatus, декодированием на месте иIsValid, созданный для мира нулевых выделений переписанной с прицелом на память платформы. - .NET 5, 2020. Поставляются шестнадцатеричные братья (
Convert.ToHexStringи друзья) - тот же дизайн-паттерн, что и у Base64, применённый к 16-символьному алфавиту, признак того, что паттерн класса-конвертера стал фирменным стилем. - .NET 6, 2021.
X509Certificate2.CreateFromPemделает PEM входом первого класса, и целый класс кода ручной разборки брони становится опциональным на современных средах выполнения. - .NET 9, ноябрь 2024.
System.Buffers.Text.Base64Urlнаконец заезжает в коробку после лет просьб сообщества, а пакетMicrosoft.Bcl.Memoryбэкопортит его для .NET Framework 4.6.2 и новее - для легаси-кодовых баз, которые всё ещё запускают всё. - .NET 11, в превью на момент написания. Следующий выпуск, которого ждут в конце 2026 года, добавляет существующим типам дополнительные удобные API и перегрузки Base64, продолжая медленное движение к более удобному интерфейсу.
Стоит держать в голове: само кодирование куда старше всего этого. Первое стандартизированное использование того, что мы теперь называем MIME Base64, - протокол Privacy-Enhanced Mail в 1987 году (RFC 989), MIME стандартизировал 76-символьную обёрнутую строками форму в 1993 году, а RFC 4648 в 2006 году дал формату его современную, знающую про алфавит спецификацию, включая URL-безопасный вариант. C# унаследовал всё это: каждая странность обёртки строками и заполнения, которую вы встретите в 30-летнем почтовом формате, - это странность, под которую C#-декодер и был спроектирован, чтобы её поглотить.
Любопытные факты о C#
- Самый маленький дымовой тест.
"TWFu"декодируется вMan. Три байта, без заполнения, без оправданий. Это hello world отладки Base64 в C#, и он прогоняет весь счастливый путь в четырёх символах. - Декодер с почтовой историей. Терпимость к пробельным символам - не случайность реализации, а дизайнерское решение, унаследованное от MIME: целое 76-символьное обёрнутое строками почтовое тело, со всеми его парами CRLF, - это валидный одиночный аргумент для
Convert.FromBase64String. Декодер был построен, чтобы съедать формат, которым почта пользуется тридцать лет. - Одна ошибка, три причины. Классическое сообщение
FormatExceptionперечисляет все три режима сбоя, которые оно, возможно, сообщает (дурной символ, слишком много заполнения, заполнение не на месте), и не говорит, какой именно сработал. Это единственное сообщение об ошибке во всём API, работающее как вопрос с вариантами ответа. - Пространство имён, которое немного врет.
System.Buffers.Textзвучит так, будто про обработку текста, но на деле это дом для преобразования двоичного в текст вообще:Utf8ParserиUtf8Formatter, которые разбирают числа и даты прямо в UTF-8, живут по соседству с классами Base64. - Заполнение необязательно на одной стороне семейства. Класс
Base64UrlдекодируетAQIDBA(шесть символов, без заполнения) иAQIDBA==(те же байты с заполнением) в те же самые четыре байта, тогда как классический декодер принимает только заполненную форму. Два декодера, два договора, одна среда выполнения. - Строки, которых не должно быть. Строка в C# по закону может содержать NUL-байты, так что
Encoding.UTF8.GetStringдекодированных двоичных может произвести «строку», полную управляющих символов, которую консоль, ваш CSV-записчик и половина JSON-библиотек на планете будут обрабатывать каждый по-своему. Система типов это разрешает; экосистема в целом - нет. - Реликт 1.1 в отличной форме.
Convert.FromBase64CharArrayимеет ту же сигнатуру из трёх параметров с апреля 2003 года, пережив революцию дженериков, революцию спанов и революцию URL-безопасности, не добавив ни одной перегрузки. Эра символьных массивов в C# не ушла; она просто отдыхает. - Одиннадцать символов, восемь байтов. Идентификаторы видео YouTube - это base64url без заполнения: 11 символов, декодирующихся в 8 байтов.
Base64Url.GetMaxDecodedLength(11)подскажет вам восьмёрку, а декодирование - одна строка, и это приятный способ закончить день, если вы из тех, кто такое пишет.
Обратное направление
Вот и вся сторона декодера, и именно здесь живёт большая часть боли, потому что декодирование - это там, где вы встречаете данные других людей: их выбор заполнения, их переводы строк, их алфавиты, их токены. Обратное направление - взять свои байты и упаковать их в Base64 - это более спокойная проблема со своим набором решений, которые надо принять, и своим набором ловушек. Кодирование Base64 в C#, от вопроса про 76 символов до URL-безопасных токенов, подробно разобрано в связанной статье ниже, и это короткое, приятное чтение, как только вы понимаете, на что смотреть.
Последнее обновление: 2026-09-08
Связанная статья: Кодирование Base64 в C# (CSharp): полное руководство