Skip to content

Commit 208b56f

Browse files
author
pfeatherstone
committed
base64 without libcrypto
1 parent 472c54f commit 208b56f

File tree

7 files changed

+182
-48
lines changed

7 files changed

+182
-48
lines changed

README.md

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@ This is an experimental replacement for Boost::Beast.
44

55
## Installation
66

7-
Copy the contents of `src` into your project then link to:
8-
- Boost::asio
9-
- OpenSSL::SSL
10-
- OpenSSL::Crypto
7+
Copy the contents of `src` into your project then link to Boost::asio. If you're using transport over TLS, then link to OpenSSL::SSL and OpenSSL::Crypto.
118

129
## Examples
1310

@@ -24,21 +21,31 @@ $ cmake ./examples -B build -DCMAKE_BUILD_TYPE=Release
2421
$ cmake --build build --parallel
2522
```
2623

24+
## Unit tests
25+
26+
Build using:
27+
28+
```bash
29+
$ cmake ./test -B build -DCMAKE_BUILD_TYPE=Release
30+
$ cmake --build build --parallel
31+
```
32+
33+
Run using:
34+
35+
```bash
36+
$ ./build/tests
37+
```
38+
2739
## Roadmap
2840
- [ ] Chunked encoding
2941
- [ ] Documentation
30-
- [ ] Unit tests
3142

3243
## Questions
3344

3445
- Q: Why not use Beast?
3546

3647
A: I find Beast bloated and unecessarily complicated. HTTP1 and WS are simple protocols. There is SO MUCH source code in Beast and I'm not convinced it's proportionate. Additionally, you don't need Beast objects like `basic_stream` or `flat_buffer`. All you need are a few structs, enums, Asio composed operations and voila.
3748

38-
- Q: Why do I need to link to openssl when I'm not using TLS?
39-
40-
A: Because of SHA1 and Base64 encoding/decoding, which are used in the websocket protocol, even without TLS. I couldn't be bothered to implement those functions, particularly as I only ever use HTTP and WS over TLS, in which case I link to openssl anyway.
41-
4249
- Q: Why are you not writing the base library Sans-IO? It's the fashion!
4350

4451
A: Because I'm only going to use this with Asio. I don't mind having state-machine logic inside an Asio composed operation rather than something custom. As far as I can tell, the only motivation for Sans-IO is unit tests. It means you don't have to open a socket.

examples/server.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,9 @@ auto handle_authorization (const http::request& req, std::string_view username_e
153153
return std::make_pair(false, "Missing Authorization field");
154154

155155
std::string_view login_base64 = lskip(field->value, "Basic ");
156-
const std::string login = http::base64_decode(login_base64);
156+
const auto login = http::base64_decode(login_base64);
157157

158-
const auto [user, passwd] = split_once(login, ":");
158+
const auto [user, passwd] = split_once(std::string_view((const char*)&login[0], login.size()), ":");
159159

160160
if (user.compare(username_exp) != 0 || passwd.compare(passwd_exp) != 0)
161161
return std::make_pair(false, "Authentication username-password don't match expected");

src/http.cpp

Lines changed: 79 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
#include <cassert>
22
#include <cstring>
3+
#include <cstdarg>
34
#include <algorithm>
45
#include <filesystem>
5-
#include <system_error>
66
#include <boost/asio/version.hpp>
7-
#include <openssl/bio.h>
8-
#include <openssl/evp.h>
9-
#include <openssl/buffer.h>
107
#include "http.h"
118

129
namespace fs = std::filesystem;
@@ -742,39 +739,90 @@ namespace http
742739

743740
//----------------------------------------------------------------------------------------------------------------
744741

745-
std::string base64_encode(std::string_view data)
742+
constexpr std::array<uint8_t, 64> base64_encode_table = {
743+
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
744+
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
745+
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
746+
};
747+
748+
constexpr std::array<uint8_t, 256> base64_decoded_table = [] {
749+
std::array<uint8_t, 256> table{};
750+
for (size_t i = 0 ; i < base64_encode_table.size() ; ++i)
751+
table[base64_encode_table[i]] = i;
752+
return table;
753+
}();
754+
755+
std::string base64_encode(const size_t ndata, const uint8_t* data)
746756
{
747-
BIO* b64 = BIO_new(BIO_f_base64());
748-
BIO* bio = BIO_new(BIO_s_mem());
749-
BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
750-
bio = BIO_push(b64, bio);
751-
752-
BIO_write(bio, data.data(), data.size());
753-
BIO_flush(bio);
757+
std::string ret;
758+
ret.reserve((ndata+2) / 3 * 4);
759+
uint8_t word{0};
760+
uint8_t off{6};
761+
762+
for (size_t i = 0 ; i < ndata ; ++i)
763+
{
764+
const uint8_t byte = data[i];
765+
766+
for (int j = 7 ; j >= 0 ; --j)
767+
{
768+
const uint8_t bit = (byte >> j) & 0x1;
769+
770+
word |= (bit << --off);
771+
772+
if (off == 0)
773+
{
774+
assert(word < 64);
775+
ret.push_back(base64_encode_table[word]);
776+
off = 6;
777+
word = 0;
778+
}
779+
}
780+
}
754781

755-
BUF_MEM* buffer_ptr{nullptr};
756-
BIO_get_mem_ptr(bio, &buffer_ptr);
782+
assert(off == 6 || off == 2 || off == 4);
757783

758-
std::string encoded(buffer_ptr->data, buffer_ptr->length);
759-
BIO_free_all(bio);
760-
return encoded;
784+
if (off < 6)
785+
{
786+
const size_t npadding = off / 2;
787+
ret.push_back(base64_encode_table[word]);
788+
for (size_t i = 0 ; i < npadding ; ++i)
789+
ret.push_back('=');
790+
}
791+
792+
return ret;
761793
}
762794

763-
std::string base64_decode(std::string_view data)
795+
std::vector<uint8_t> base64_decode(std::string_view data)
764796
{
765-
BIO* b64 = BIO_new(BIO_f_base64());
766-
BIO* bio = BIO_new_mem_buf(data.data(), data.size());
767-
BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
768-
bio = BIO_push(b64, bio);
769-
770-
std::string output(data.size(), '\0'); // Base64 expands by 4/3, so input is always >= output
771-
int decoded_len = BIO_read(bio, output.data(), output.size());
772-
if (decoded_len < 0)
773-
fprintf(stderr, "Failed to base64 decode data\n");
774-
775-
output.resize(std::max(decoded_len, 0));
776-
BIO_free_all(bio);
777-
return output;
797+
std::vector<uint8_t> ret;
798+
ret.reserve(data.size() / 4 * 3);
799+
uint8_t word{0};
800+
uint8_t off{8};
801+
802+
for (size_t i = 0 ; i < data.size() ; ++i)
803+
{
804+
if (data[i] == '=')
805+
continue;
806+
807+
const uint8_t sixtet = base64_decoded_table[data[i]];
808+
809+
for (int j = 5 ; j >= 0 ; --j)
810+
{
811+
const uint8_t bit = (sixtet >> j) & 0x1;
812+
813+
word |= (bit << --off);
814+
815+
if (off == 0)
816+
{
817+
assert(word < 256);
818+
ret.push_back(word);
819+
off = 8;
820+
word = 0;
821+
}
822+
}
823+
}
824+
825+
return ret;
778826
}
779827

780828
//----------------------------------------------------------------------------------------------------------------

src/http.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -508,8 +508,8 @@ namespace http
508508

509509
//----------------------------------------------------------------------------------------------------------------
510510

511-
std::string base64_encode(std::string_view data);
512-
std::string base64_decode(std::string_view data);
511+
std::string base64_encode(const size_t ndata, const uint8_t* data);
512+
std::vector<uint8_t> base64_decode(std::string_view data);
513513

514514
//----------------------------------------------------------------------------------------------------------------
515515

src/http_async.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,7 @@ namespace http
464464
const auto hash = sha1{}.push(sec_ws_key.size(), (const uint8_t*)sec_ws_key.data())
465465
.push(magic.size(), (const uint8_t*)magic.data())
466466
.finish();
467-
return base64_encode(std::string_view{(const char*)&hash[0], hash.size()});
467+
return base64_encode(hash.size(), &hash[0]);
468468
}
469469

470470
template<class AsyncStream>
@@ -473,7 +473,7 @@ namespace http
473473
AsyncStream& sock;
474474
std::string uri;
475475
std::string host;
476-
char nonce[16] = {0};
476+
uint8_t nonce[16] = {0};
477477
std::unique_ptr<std::string> buf = std::make_unique<std::string>();
478478
std::unique_ptr<request> req = std::make_unique<request>();
479479
std::unique_ptr<response> reply = std::make_unique<response>();
@@ -512,7 +512,7 @@ namespace http
512512
req->add_header(field::connection, "upgrade");
513513
req->add_header(field::upgrade, "websocket");
514514
req->add_header(field::sec_websocket_version, "13");
515-
req->add_header(field::sec_websocket_key, base64_encode(std::string_view(nonce, 16)));
515+
req->add_header(field::sec_websocket_key, base64_encode(16, nonce));
516516

517517
async_http_write(sock, *req, *buf, std::move(self));
518518
}
@@ -542,7 +542,7 @@ namespace http
542542

543543
if (sec_ws_accept == end(reply->headers))
544544
self.complete(make_error_code(ws_handshake_missing_seq_accept));
545-
else if (sec_ws_accept->value != compute_sec_ws_accept(base64_encode(std::string_view(nonce, 16))))
545+
else if (sec_ws_accept->value != compute_sec_ws_accept(base64_encode(16, nonce)))
546546
self.complete(make_error_code(ws_handshake_bad_sec_accept));
547547
else
548548
self.complete({});

test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ target_link_libraries(http
2828
# Unit tests
2929
add_executable(tests
3030
main.cpp
31+
base64.cpp
3132
sha1.cpp)
3233
target_link_options(tests PUBLIC $<$<CONFIG:Release>:-s>)
3334
target_link_libraries(tests PUBLIC http)

test/base64.cpp

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#include <random>
2+
#include <algorithm>
3+
#include <openssl/bio.h>
4+
#include <openssl/evp.h>
5+
#include <openssl/buffer.h>
6+
#include <http.h>
7+
#include "doctest.h"
8+
9+
static std::string openssl_base64_encode(const size_t ndata, const uint8_t* data)
10+
{
11+
BIO* b64 = BIO_new(BIO_f_base64());
12+
BIO* bio = BIO_new(BIO_s_mem());
13+
BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
14+
bio = BIO_push(b64, bio);
15+
16+
BIO_write(bio, data, ndata);
17+
BIO_flush(bio);
18+
19+
BUF_MEM* buffer_ptr{nullptr};
20+
BIO_get_mem_ptr(bio, &buffer_ptr);
21+
22+
std::string encoded(buffer_ptr->data, buffer_ptr->length);
23+
BIO_free_all(bio);
24+
return encoded;
25+
}
26+
27+
static std::vector<uint8_t> openssl_base64_decode(std::string_view data)
28+
{
29+
BIO* b64 = BIO_new(BIO_f_base64());
30+
BIO* bio = BIO_new_mem_buf(data.data(), data.size());
31+
BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
32+
bio = BIO_push(b64, bio);
33+
34+
std::vector<uint8_t> output(data.size(), 0); // Base64 expands by 4/3, so input is always >= output
35+
int decoded_len = BIO_read(bio, output.data(), output.size());
36+
if (decoded_len < 0)
37+
fprintf(stderr, "Failed to base64 decode data\n");
38+
39+
output.resize(std::max(decoded_len, 0));
40+
BIO_free_all(bio);
41+
return output;
42+
}
43+
44+
TEST_SUITE("[BASE64]")
45+
{
46+
TEST_CASE("matches openssl")
47+
{
48+
std::mt19937 eng(std::random_device{}());
49+
std::uniform_int_distribution<uint8_t> d{};
50+
std::vector<uint8_t> buf;
51+
buf.reserve(4096);
52+
53+
for (size_t i = 0 ; i < 10000 ; ++i)
54+
{
55+
// Fill with random
56+
buf.resize(i);
57+
std::generate(begin(buf), end(buf), [&]{return d(eng);});
58+
59+
// Encode
60+
auto encoded_ssl = openssl_base64_encode(buf.size(), buf.data());
61+
auto encoded_custom = http::base64_encode(buf.size(), buf.data());
62+
63+
// Check
64+
REQUIRE(encoded_ssl == encoded_custom);
65+
66+
// Decode with padding
67+
auto decoded = http::base64_decode(encoded_custom);
68+
REQUIRE(buf.size() == decoded.size());
69+
REQUIRE(std::equal(begin(buf), end(buf), begin(decoded)));
70+
71+
// Decode without padding
72+
encoded_custom.erase(std::remove(begin(encoded_custom), end(encoded_custom), '='), end(encoded_custom));
73+
decoded = http::base64_decode(encoded_custom);
74+
REQUIRE(buf.size() == decoded.size());
75+
REQUIRE(std::equal(begin(buf), end(buf), begin(decoded)));
76+
}
77+
}
78+
}

0 commit comments

Comments
 (0)