Кодирование и декодирование Base64 с помощью OpenSSL

Я пытался выяснить документацию openssl для декодирования и кодирования base64. Я нашел несколько фрагментов кода ниже

#include <openssl/sha.h>
#include <openssl/hmac.h>
#include <openssl/evp.h>
#include <openssl/bio.h>
#include <openssl/buffer.h>

char *base64(const unsigned char *input, int length)
{
  BIO *bmem, *b64;
  BUF_MEM *bptr;

  b64 = BIO_new(BIO_f_base64());
  bmem = BIO_new(BIO_s_mem());
  b64 = BIO_push(b64, bmem);
  BIO_write(b64, input, length);
  BIO_flush(b64);
  BIO_get_mem_ptr(b64, &bptr);

  char *buff = (char *)malloc(bptr->length);
  memcpy(buff, bptr->data, bptr->length-1);
  buff[bptr->length-1] = 0;

  BIO_free_all(b64);

  return buff;
}

char *decode64(unsigned char *input, int length)
{
  BIO *b64, *bmem;

  char *buffer = (char *)malloc(length);
  memset(buffer, 0, length);

  b64 = BIO_new(BIO_f_base64());
  bmem = BIO_new_mem_buf(input, length);
  bmem = BIO_push(b64, bmem);

  BIO_read(bmem, buffer, length);

  BIO_free_all(bmem);

  return buffer;
}

Кажется, это работает только для однострочных строк, таких как «Начало», в тот момент, когда я ввожу сложные строки с новой строкой, пробелами и т. Д., Это ужасно не работает.

Это даже не обязательно должен быть openssl, простой класс или набор функций, которые делают то же самое, было бы хорошо, это очень сложный процесс сборки для решения, и я пытаюсь избежать необходимости заходить туда и вносить несколько изменений . Единственная причина, по которой я выбрал openssl, заключается в том, что решение уже скомпилировано с библиотеками.


person Bernard    schedule 13.03.2011    source источник
comment
Ответ на чистом OpenSSL: stackoverflow.com/a/33331627/795876 :)   -  person fsenart    schedule 25.10.2015
comment
Есть аналогичный вопрос, он для C, но там есть ответы C ++: stackoverflow.com/a/34201175/5447906   -  person anton_rh    schedule 10.12.2015


Ответы (10)


Лично я считаю, что использование OpenSSL API настолько болезненно, что я избегаю его, если только цена избегания не очень высока. Меня очень огорчает то, что он стал стандартным API в мире криптографии.

Мне было скучно, и я написал вам один на C ++. Это должно даже обрабатывать крайние случаи, которые могут вызвать проблемы с безопасностью, такие как, например, кодирование строки, которая приводит к целочисленному переполнению, потому что она слишком велика.

Я провел несколько модульных тестов, так что все должно работать.

#include <string>
#include <cassert>
#include <limits>
#include <stdexcept>
#include <cctype>

static const char b64_table[65] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

static const char reverse_table[128] = {
   64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
   64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
   64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 62, 64, 64, 64, 63,
   52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 64, 64, 64, 64, 64, 64,
   64,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14,
   15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 64, 64, 64, 64, 64,
   64, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
   41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 64, 64, 64, 64, 64
};

::std::string base64_encode(const ::std::string &bindata)
{
   using ::std::string;
   using ::std::numeric_limits;

   if (bindata.size() > (numeric_limits<string::size_type>::max() / 4u) * 3u) {
      throw ::std::length_error("Converting too large a string to base64.");
   }

   const ::std::size_t binlen = bindata.size();
   // Use = signs so the end is properly padded.
   string retval((((binlen + 2) / 3) * 4), '=');
   ::std::size_t outpos = 0;
   int bits_collected = 0;
   unsigned int accumulator = 0;
   const string::const_iterator binend = bindata.end();

   for (string::const_iterator i = bindata.begin(); i != binend; ++i) {
      accumulator = (accumulator << 8) | (*i & 0xffu);
      bits_collected += 8;
      while (bits_collected >= 6) {
         bits_collected -= 6;
         retval[outpos++] = b64_table[(accumulator >> bits_collected) & 0x3fu];
      }
   }
   if (bits_collected > 0) { // Any trailing bits that are missing.
      assert(bits_collected < 6);
      accumulator <<= 6 - bits_collected;
      retval[outpos++] = b64_table[accumulator & 0x3fu];
   }
   assert(outpos >= (retval.size() - 2));
   assert(outpos <= retval.size());
   return retval;
}

::std::string base64_decode(const ::std::string &ascdata)
{
   using ::std::string;
   string retval;
   const string::const_iterator last = ascdata.end();
   int bits_collected = 0;
   unsigned int accumulator = 0;

   for (string::const_iterator i = ascdata.begin(); i != last; ++i) {
      const int c = *i;
      if (::std::isspace(c) || c == '=') {
         // Skip whitespace and padding. Be liberal in what you accept.
         continue;
      }
      if ((c > 127) || (c < 0) || (reverse_table[c] > 63)) {
         throw ::std::invalid_argument("This contains characters not legal in a base64 encoded string.");
      }
      accumulator = (accumulator << 6) | reverse_table[c];
      bits_collected += 6;
      if (bits_collected >= 8) {
         bits_collected -= 8;
         retval += static_cast<char>((accumulator >> bits_collected) & 0xffu);
      }
   }
   return retval;
}
person Omnifarious    schedule 13.03.2011
comment
Просто вопрос новичка !! когда мы возвращаем retval, разве это не переменная стека, которую не следует возвращать? поскольку ее можно удалить, как только функция выйдет за пределы области видимости? - person bana; 25.10.2013
comment
@bana - функция возвращает новый объект std :: string, который назначается до того, как объект стека будет уничтожен, так что это безопасно. - person nevelis; 15.12.2013
comment
@Omnifarious: -1 - хотя это вполне приемлемое решение, вопрос был задан, как это сделать в OpenSSL, поэтому этот ответ не имеет отношения к исходному вопросу и не помог мне;) Вы находите, что это расстраивает, что OpenSSL отраслевой стандарт - можете ли вы предложить альтернативу (не делая этого самостоятельно)? Библиотека OpenSSL широко использует машинно-зависимую сборку во многих областях для повышения производительности, а API ориентирован на потоковую обработку; может быть, поэтому вы расстраиваетесь из-за написания блочной оболочки. Отказ от ответственности: я не поддерживаю OpenSSL. - person nevelis; 15.12.2013
comment
@Omnifarious вот мой чистый ответ OpenSSL, не могли бы вы просмотреть, пожалуйста. stackoverflow.com/a/33331627/795876 - person fsenart; 25.10.2015
comment
@nevelis - я посмотрю, как написать версию с использованием OpenSSL API, которая соответствует C ++. К сожалению, альтернативы разрознены, и я не знаю, что какие-то из них лучше. И меня особенно беспокоит не потоковая природа API. Хотя вроде как. OpenSSL вообще не должен заботиться о таких вещах, как файловые дескрипторы, потоки или что-то еще. Его можно использовать на любом транспорте, и вызывающий должен обрабатывать детали. Меня больше беспокоит излишняя бюрократия API. Столько шагов по малейшему, без видимой причины. - person Omnifarious; 07.08.2017
comment
Большая сложность OpenSSL возникает из-за нескольких вещей: от адского макроса до недокументированных внутренних компонентов, которые вам нужно использовать, чтобы делать то, что даже .NET может делать с одной строкой кода, до попыток поддерживать каждую платформу с таким же повторным использованием, как возможно, пытаясь не добавлять новый, непроверенный код, а использовать то, что там есть ... и в любом случае вы получите мусор. Вы правы насчет альтернатив, особенно тех, которые пытаются его очистить ... Вы можете полировать какашку сколько угодно, но это все равно какашка. :) - person nevelis; 08.08.2017
comment
Или итератор для преобразования символов в base64: codereview.stackexchange.com/questions/248339/base64 -итераторы - person Martin York; 02.09.2020

Вместо интерфейса BIO_ гораздо проще использовать интерфейс EVP_ . Например:

#include <iostream>
#include <stdlib.h>
#include <openssl/evp.h>

char *base64(const unsigned char *input, int length) {
  const auto pl = 4*((length+2)/3);
  auto output = reinterpret_cast<char *>(calloc(pl+1, 1)); //+1 for the terminating null that EVP_EncodeBlock adds on
  const auto ol = EVP_EncodeBlock(reinterpret_cast<unsigned char *>(output), input, length);
  if (pl != ol) { std::cerr << "Whoops, encode predicted " << pl << " but we got " << ol << "\n"; }
  return output;
}

unsigned char *decode64(const char *input, int length) {
  const auto pl = 3*length/4;
  auto output = reinterpret_cast<unsigned char *>(calloc(pl+1, 1));
  const auto ol = EVP_DecodeBlock(output, reinterpret_cast<const unsigned char *>(input), length);
  if (pl != ol) { std::cerr << "Whoops, decode predicted " << pl << " but we got " << ol << "\n"; }
  return output;
}

Функции EVP также включают потоковый интерфейс, см. Справочную страницу.

person mtrw    schedule 07.03.2020
comment
Так много примеров, делающих это на собственном горьком опыте. Спасибо за размещение метода EVP_EncodeBlock! - person Shibumi; 08.05.2020
comment
Спасибо! Это лучший способ сделать это. Простой. - person rodolk; 14.07.2020
comment
(1) результат decode64 () является двоичным, поэтому требуется длина - например, может вернуть std :: pair ‹unsigned char *, size_t›. (2) Каждый вызов calloc () требует вызова free (). (3) длина вывода decode64 () должна быть уменьшена при наличии заполнения ввода. - person badfd; 20.07.2021

Вот пример кодирования / декодирования OpenSSL base64, который я написал:

Заметьте, у меня есть несколько макросов / классов в написанном мной коде, но ни один из них не важен для примера. Я написал просто несколько оберток C ++:

buffer base64::encode( const buffer& data )
{
    // bio is simply a class that wraps BIO* and it free the BIO in the destructor.

    bio b64(BIO_f_base64()); // create BIO to perform base64
    BIO_set_flags(b64,BIO_FLAGS_BASE64_NO_NL);

    bio mem(BIO_s_mem()); // create BIO that holds the result

    // chain base64 with mem, so writing to b64 will encode base64 and write to mem.
    BIO_push(b64, mem);

    // write data
    bool done = false;
    int res = 0;
    while(!done)
    {
        res = BIO_write(b64, data.data, (int)data.size);

        if(res <= 0) // if failed
        {
            if(BIO_should_retry(b64)){
                continue;
            }
            else // encoding failed
            {
                /* Handle Error!!! */
            }
        }
        else // success!
            done = true;
    }

    BIO_flush(b64);

    // get a pointer to mem's data
    char* dt;
    long len = BIO_get_mem_data(mem, &dt);

    // assign data to output
    std::string s(dt, len);

    return buffer(s.length()+sizeof(char), (byte*)s.c_str());
}
person TCS    schedule 16.03.2011

Это сработало для меня и проверило отсутствие утечек памяти с помощью valgrind.

#include <openssl/bio.h>
#include <openssl/evp.h>
#include <cstring>
#include <memory>
#include <string>
#include <vector>

#include <iostream>

namespace {
struct BIOFreeAll { void operator()(BIO* p) { BIO_free_all(p); } };
}

std::string Base64Encode(const std::vector<unsigned char>& binary)
{
    std::unique_ptr<BIO,BIOFreeAll> b64(BIO_new(BIO_f_base64()));
    BIO_set_flags(b64.get(), BIO_FLAGS_BASE64_NO_NL);
    BIO* sink = BIO_new(BIO_s_mem());
    BIO_push(b64.get(), sink);
    BIO_write(b64.get(), binary.data(), binary.size());
    BIO_flush(b64.get());
    const char* encoded;
    const long len = BIO_get_mem_data(sink, &encoded);
    return std::string(encoded, len);
}

// Assumes no newlines or extra characters in encoded string
std::vector<unsigned char> Base64Decode(const char* encoded)
{
    std::unique_ptr<BIO,BIOFreeAll> b64(BIO_new(BIO_f_base64()));
    BIO_set_flags(b64.get(), BIO_FLAGS_BASE64_NO_NL);
    BIO* source = BIO_new_mem_buf(encoded, -1); // read-only source
    BIO_push(b64.get(), source);
    const int maxlen = strlen(encoded) / 4 * 3 + 1;
    std::vector<unsigned char> decoded(maxlen);
    const int len = BIO_read(b64.get(), decoded.data(), maxlen);
    decoded.resize(len);
    return decoded;
}

int main()
{
    const char* msg = "hello";
    const std::vector<unsigned char> binary(msg, msg+strlen(msg));
    const std::string encoded = Base64Encode(binary);
    std::cout << "encoded = " << encoded << std::endl;
    const std::vector<unsigned char> decoded = Base64Decode(encoded.c_str());
    std::cout << "decoded = ";
    for (unsigned char c : decoded) std::cout << c;
    std::cout << std::endl;
    return 0;
}

Скомпилировать:

g++ -lcrypto main.cc

Вывод:

encoded = aGVsbG8=
decoded = hello
person Matt    schedule 26.12.2017

Улучшенный ответ TCS для удаления макросов / структур данных.

unsigned char *encodeb64mem( unsigned char *data, int len, int *lenoutput )
{
// bio is simply a class that wraps BIO* and it free the BIO in the destructor.

BIO *b64 = BIO_new(BIO_f_base64()); // create BIO to perform base64
BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);

BIO *mem = BIO_new(BIO_s_mem()); // create BIO that holds the result

// chain base64 with mem, so writing to b64 will encode base64 and write to mem.
BIO_push(b64, mem);

// write data
bool done = false;
int res = 0;
while(!done)
{
    res = BIO_write(b64, data, len);

    if(res <= 0) // if failed
    {
        if(BIO_should_retry(b64)){
            continue;
        }
        else // encoding failed
        {
            /* Handle Error!!! */
        }
    }
    else // success!
        done = true;
}

BIO_flush(b64);

// get a pointer to mem's data
unsigned char* output;
*lenoutput = BIO_get_mem_data(mem, &output);

// assign data to output
//std::string s(dt, len2);

return output;
}

Записать в файл

int encodeb64(unsigned char* input, const char* filenm, int leni)
{
BIO *b64 = BIO_new(BIO_f_base64());
BIO_set_flags(b64,BIO_FLAGS_BASE64_NO_NL);

BIO *file = BIO_new_file(filenm, "w");
BIO *mem = BIO_new(BIO_f_buffer());
BIO_push(b64, mem);
BIO_push(mem, file);

// write data
bool done = false;
int res = 0;
while(!done)
{
    res = BIO_write(b64, input, leni);

    if(res <= 0) // if failed
    {
        if(BIO_should_retry(b64)){
            continue;
        }
        else // encoding failed
        {
            /* Handle Error!!! */
        }
    }
    else // success!
        done = true;
}

BIO_flush(b64);
BIO_pop(b64);
BIO_free_all(b64);

    return 0;
}

Кодировка Base64 от файла к файлу. Много раз из-за ограничений файла мы читали фрагменты данных и выполняли кодирование. Ниже приведен код.

int encodeb64FromFile(const char* input, const char* outputfilename)
{
BIO *b64 = BIO_new(BIO_f_base64());
BIO_set_flags(b64,BIO_FLAGS_BASE64_NO_NL);
int leni = 3*64;
unsigned char *data[3*64];
BIO *file = BIO_new_file(outputfilename, "w");
BIO *mem = BIO_new(BIO_f_buffer());
BIO_push(b64, mem);
BIO_push(mem, file);

FILE *fp = fopen(input, "rb");
while ((leni = fread(data, 1, sizeof data, fp)) > 0) {
    // write data
    bool done = false;
    int res = 0;
    while(!done)
    {
        res = BIO_write(b64, data, leni);

        if(res <= 0) // if failed
        {
            if(BIO_should_retry(b64)){
                continue;
            }
            else // encoding failed
            {
                /* Handle Error!!! */
            }
        }
        else // success!
            done = true;
    }

 }

 BIO_flush(b64);
BIO_pop(b64);
BIO_free_all(b64);
fclose(fp);

return 0;
 }
person Satish    schedule 09.06.2013
comment
Есть ли причина, по которой encodeb64mem не вызывает BIO_free_all? Разве это не утечка памяти? - person Jason Rice; 23.09.2015

Мне нравится использование EVP mtrw.

Ниже приведен мой современный ответ на C ++ без выделения памяти вручную (calloc). Это займет std::string, но его можно легко перегрузить, например, используя необработанные байты.

#include <openssl/evp.h>

#include <memory>
#include <stdexcept>
#include <vector>


auto EncodeBase64(const std::string& to_encode) -> std::string {
  /// @sa https://www.openssl.org/docs/manmaster/man3/EVP_EncodeBlock.html

  const auto predicted_len = 4 * ((to_encode.length() + 2) / 3);  // predict output size

  const auto output_buffer{std::make_unique<char[]>(predicted_len + 1)};

  const std::vector<unsigned char> vec_chars{to_encode.begin(), to_encode.end()};  // convert to_encode into uchar container

  const auto output_len = EVP_EncodeBlock(reinterpret_cast<unsigned char*>(output_buffer.get()), vec_chars.data(), static_cast<int>(vec_chars.size()));

  if (predicted_len != static_cast<unsigned long>(output_len)) {
    throw std::runtime_error("EncodeBase64 error");
  }

  return output_buffer.get();
}

auto DecodeBase64(const std::string& to_decode) -> std::string {
  /// @sa https://www.openssl.org/docs/manmaster/man3/EVP_DecodeBlock.html

  const auto predicted_len = 3 * to_decode.length() / 4;  // predict output size

  const auto output_buffer{std::make_unique<char[]>(predicted_len + 1)};

  const std::vector<unsigned char> vec_chars{to_decode.begin(), to_decode.end()};  // convert to_decode into uchar container

  const auto output_len = EVP_DecodeBlock(reinterpret_cast<unsigned char*>(output_buffer.get()), vec_chars.data(), static_cast<int>(vec_chars.size()));

  if (predicted_len != static_cast<unsigned long>(output_len)) {
    throw std::runtime_error("DecodeBase64 error");
  }

  return output_buffer.get();
}

Вероятно, есть более чистый / лучший способ сделать это (я бы хотел избавиться от reinterpret_cast). Вы также определенно захотите, чтобы блок try/catch имел дело с потенциальным исключением.

person Simog    schedule 02.09.2020

Base64 действительно довольно прост; у вас не должно возникнуть проблем с поиском любого количества реализаций через быстрый Google. Например, здесь - это эталонная реализация на языке C из Консорциум программного обеспечения Интернета, с подробными комментариями, объясняющими процесс.

Реализация openssl усложняет работу с "BIO", которая (ИМХО) не очень полезна, если все, что вы делаете, - это декодирование / кодирование.

person David Gelhar    schedule 13.03.2011

Так много ужасных C примеров кода с буферами и malloc(), как насчет правильного std::string использования этого C++ вопроса с тегами?

#include <openssl/bio.h>
#include <openssl/evp.h>
#include <openssl/buffer.h>
#include <string>

std::string base64_encode(const std::string& input)
{
    const auto base64_memory = BIO_new(BIO_s_mem());
    auto base64 = BIO_new(BIO_f_base64());
    base64 = BIO_push(base64, base64_memory);
    BIO_write(base64, input.c_str(), static_cast<int>(input.length()));
    BIO_flush(base64);
    BUF_MEM* buffer_memory{};
    BIO_get_mem_ptr(base64, &buffer_memory);
    auto base64_encoded = std::string(buffer_memory->data, buffer_memory->length - 1);
    BIO_free_all(base64);
    return base64_encoded;
}
person BullyWiiPlaza    schedule 23.05.2021

Поздно к вечеринке, но я сам недавно столкнулся с этой проблемой, но был недоволен как решением BIO, которое излишне запутано, но также не понравилось 'EncodeBlock', потому что оно вводит символы новой строки, которые я не хочу в моем кодированном Base64 нить.

После небольшого обнюхивания я наткнулся на файл заголовка openssl/include/crypto/evp.h, который не является частью установки по умолчанию (который экспортирует только папку include / openssl для меня), но экспортирует решение проблемы.

void evp_encode_ctx_set_flags(EVP_ENCODE_CTX *ctx, unsigned int flags);

/* EVP_ENCODE_CTX flags */
/* Don't generate new lines when encoding */
#define EVP_ENCODE_CTX_NO_NEWLINES          1
/* Use the SRP base64 alphabet instead of the standard one */
#define EVP_ENCODE_CTX_USE_SRP_ALPHABET     2

Используя эту функцию, становится возможным «без новой строки» с помощью интерфейса EVP.

Пример:

if (EVP_ENCODE_CTX *context = EVP_ENCODE_CTX_new())
{
    EVP_EncodeInit(context);
    evp_encode_ctx_set_flags(context, EVP_ENCODE_CTX_NO_NEWLINES);
    while (hasData())
    {
        uint8_t *data;
        int32_t length = fetchData(&data);
        int32_t size = (((EVP_ENCODE_CTX_num(context) + length)/48) * 65) + 1;
        uint8_t buffer[size];
        EVP_EncodeUpdate(context, buffer, &size, pData, length);
        //process encoded data.
    }
    uint8_t buffer[65];
    int32_t writtenBytes;
    EVP_EncodeFinal(context, buffer, &writtenBytes);
    //Do something with the final remainder of the encoded string.
    EVP_ENCODE_CTX_free(context);
}

Этот фрагмент кода закодирует буфер в Base64 без символов новой строки. Обратите внимание на использование EVP_ENCODE_CTX_num для получения «оставшихся байтов», все еще хранящихся в объекте контекста, для вычисления правильного размера буфера.

Это необходимо только в том случае, если вам нужно вызывать EVP_EncodeUpdate несколько раз, потому что ваши данные слишком велики или недоступны сразу.

person Refugnic Eternium    schedule 16.02.2021

person    schedule
comment
Извините, это письмо спустя годы после вашего запроса на проверку, но ... Не приводите к void *, особенно в C ++, но никогда, даже в C.Кроме того, вообще не используйте приведение в стиле C в C ++ (это (type) приведение типов). Всегда используйте static_cast, const_cast или reinterpret_cast и используйте минимально мощное приведение для выполнения задачи. Также не используйте malloc в коде C ++. Вы также не должны позволять голым указателям выходить за пределы функций, которые имеют дело с OpenSSL. out_len должен быть ссылкой, а не указателем. По сути, это решение на языке C, а не на языке C ++. - person Omnifarious; 04.08.2017
comment
@ Великолепный обзор кода. он сказал, что это решение только для OpenSSL, а не для C ++. Я нашел это полезным в качестве отправной точки много лет назад, и хотя мне пришлось многое очистить, как вы предлагали, это все еще решение только для OpenSSL, которое умещается на одной странице. Если вы пытаетесь критиковать ответ, почему бы не сделать такую ​​вещь и не предложить исправление кода? - person nevelis; 08.08.2017
comment
@nevelis - в вопросе использовался тег C ++, поэтому я предположил, что плакат хотел получить ответ C ++. - person Omnifarious; 03.05.2021