Skip to content

Commit 86b154a

Browse files
authored
Optimize base64 writing to XML and reuse Enc buffer (#72)
Signed-off-by: Raul Metsma <[email protected]>
1 parent 8cc9926 commit 86b154a

File tree

9 files changed

+162
-60
lines changed

9 files changed

+162
-60
lines changed

.github/workflows/build.yml

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,24 @@ jobs:
5656
steps:
5757
- name: Checkout
5858
uses: actions/checkout@v4
59+
- name: Expose Android NDK env
60+
shell: bash
61+
run: |
62+
echo "ANDROID_NDK_HOME=$ANDROID_NDK_LATEST_HOME" >> "$GITHUB_ENV"
63+
echo "ANDROID_NDK_ROOT=$ANDROID_NDK_LATEST_HOME" >> "$GITHUB_ENV"
64+
- name: Cache vcpkg
65+
uses: actions/cache@v4
66+
with:
67+
path: ${{ github.workspace }}/vcpkg_cache
68+
key: vcpkg-${{ matrix.target }}-${{ hashFiles('.github/workflows/*', 'vcpkg.json', 'CMakeLists.txt', '**/CMakeLists.txt', 'CMakePresets.json') }}
5969
- name: Prepare vcpkg
6070
uses: lukka/run-vcpkg@v11
6171
with:
62-
vcpkgGitCommitId: 031ad89ce6c575df35a8e58707ad2c898446c63e
72+
vcpkgGitCommitId: 085820b35f4ef5ad54967c8a46fb822e53c4be33
6373
vcpkgJsonGlob: ./vcpkg.json
6474
runVcpkgInstall: true
6575
env:
76+
VCPKG_BINARY_SOURCES: clear;files,${{ github.workspace }}/vcpkg_cache,readwrite
6677
VCPKG_DEFAULT_TRIPLET: ${{ matrix.triplet }}
6778
- name: Build
6879
run: |
@@ -95,14 +106,21 @@ jobs:
95106
run: |
96107
brew update
97108
brew install --formula flatbuffers swig doxygen boost
109+
- name: Cache vcpkg
110+
if: matrix.target != 'macos'
111+
uses: actions/cache@v4
112+
with:
113+
path: ${{ github.workspace }}/vcpkg_cache
114+
key: vcpkg-${{ matrix.target }}-${{ hashFiles('.github/workflows/*', 'vcpkg.json', 'CMakeLists.txt', '**/CMakeLists.txt', 'CMakePresets.json') }}
98115
- name: Prepare vcpkg
99116
if: matrix.target != 'macos'
100117
uses: lukka/run-vcpkg@v11
101118
with:
102-
vcpkgGitCommitId: 031ad89ce6c575df35a8e58707ad2c898446c63e
119+
vcpkgGitCommitId: 085820b35f4ef5ad54967c8a46fb822e53c4be33
103120
vcpkgJsonGlob: ./vcpkg.json
104121
runVcpkgInstall: true
105122
env:
123+
VCPKG_BINARY_SOURCES: clear;files,${{ github.workspace }}/vcpkg_cache,readwrite
106124
VCPKG_DEFAULT_TRIPLET: ${{ matrix.triplet }}
107125
- name: Build
108126
run: |
@@ -132,14 +150,20 @@ jobs:
132150
steps:
133151
- name: Checkout
134152
uses: actions/checkout@v4
153+
- name: Cache vcpkg
154+
uses: actions/cache@v4
155+
with:
156+
path: ${{ github.workspace }}/vcpkg_cache
157+
key: vcpkg-${{ matrix.image }}-${{ matrix.platform }}-${{ hashFiles('.github/workflows/*', 'vcpkg.json', 'CMakeLists.txt', '**/CMakeLists.txt', 'CMakePresets.json') }}
135158
- name: Prepare vcpkg
136159
uses: lukka/run-vcpkg@v11
137160
with:
138-
vcpkgGitCommitId: 031ad89ce6c575df35a8e58707ad2c898446c63e
161+
vcpkgGitCommitId: 085820b35f4ef5ad54967c8a46fb822e53c4be33
139162
vcpkgJsonGlob: ./vcpkg.json
140163
runVcpkgInstall: true
141164
runVcpkgFormatString: "[`install`, `--recurse`, `--clean-after-build`, `--x-install-root`, `$[env.VCPKG_INSTALLED_DIR]`, `--triplet`, `$[env.VCPKG_DEFAULT_TRIPLET]`, `--x-feature`, `tests`]"
142165
env:
166+
VCPKG_BINARY_SOURCES: clear;files,${{ github.workspace }}/vcpkg_cache,readwrite
143167
VCPKG_INSTALLED_DIR: ${{ github.workspace }}/build/vcpkg_installed
144168
- name: Install dependencies
145169
run: winget install --silent --accept-source-agreements --accept-package-agreements swig doxygen

cdoc/CDoc1Writer.cpp

Lines changed: 39 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ struct CDoc1Writer::Private final: public XMLWriter
5959
std::vector<Recipient> rcpts;
6060

6161
int64_t writeEncryptionProperties(bool use_ddoc);
62-
int64_t writeKeyInfo(bool use_ddoc, const Crypto::Key& transportKey);
62+
int64_t writeKeyInfo(bool use_ddoc, const std::vector<Recipient> &rcpts, const Crypto::Key& transportKey);
6363
int64_t writeRecipient(const std::vector<uint8_t> &recipient, const Crypto::Key& transportKey);
6464
};
6565

@@ -90,7 +90,7 @@ int64_t CDoc1Writer::Private::writeEncryptionProperties(bool use_ddoc)
9090
return writeEndElement(Private::DENC); // EncryptedData
9191
}
9292

93-
int64_t CDoc1Writer::Private::writeKeyInfo(bool use_ddoc, const Crypto::Key& transportKey)
93+
int64_t CDoc1Writer::Private::writeKeyInfo(bool use_ddoc, const std::vector<Recipient> &rcpts, const Crypto::Key& transportKey)
9494
{
9595
RET_ERROR(writeStartElement(Private::DENC, "EncryptedData",
9696
{{"MimeType", use_ddoc ? "http://www.sk.ee/DigiDoc/v1.3.0/digidoc.xsd" : "application/octet-stream"}}));
@@ -230,43 +230,38 @@ int64_t CDoc1Writer::Private::writeRecipient(const std::vector<uint8_t> &recipie
230230
libcdoc::result_t
231231
CDoc1Writer::encrypt(libcdoc::MultiDataSource& src, const std::vector<libcdoc::Recipient>& keys)
232232
{
233+
if(keys.empty())
234+
return WORKFLOW_ERROR;
233235
RET_ERROR(beginEncryption());
234236
d->rcpts = keys;
235237
Crypto::Key transportKey = Crypto::generateKey(d->method);
236238
int n_components = src.getNumComponents();
237239
bool use_ddoc = (n_components > 1) || (n_components == libcdoc::NOT_IMPLEMENTED);
238240

239-
RET_ERROR(d->writeKeyInfo(use_ddoc, transportKey));
240-
RET_ERROR(d->writeElement(Private::DENC, "CipherData", [&]() -> int64_t {
241-
std::vector<uint8_t> data;
242-
data.reserve(16384);
243-
VectorConsumer vcons(data);
244-
EncryptionConsumer enc(vcons, d->method, transportKey);
245-
std::string name;
246-
int64_t size;
247-
if(use_ddoc) {
248-
DDOCWriter ddoc(enc);
249-
result_t result;
250-
for (result = src.next(name, size); result == OK; result = src.next(name, size)) {
251-
std::vector<uint8_t> contents;
252-
VectorConsumer vcons(contents);
253-
RET_ERROR(src.readAll(vcons));
254-
RET_ERROR(vcons.close());
255-
RET_ERROR(ddoc.addFile(name, "application/octet-stream", contents));
256-
d->files.push_back({name, contents.size()});
241+
RET_ERROR(d->writeKeyInfo(use_ddoc, keys, transportKey));
242+
RET_ERROR(d->writeElement(Private::DENC, "CipherData", [&] {
243+
return d->writeBase64Element(Private::DENC, "CipherValue", [&](DataConsumer &dst) -> int64_t {
244+
EncryptionConsumer enc(dst, d->method, transportKey);
245+
std::string name;
246+
int64_t size;
247+
if(use_ddoc) {
248+
DDOCWriter ddoc(enc);
249+
result_t result;
250+
for (result = src.next(name, size); result == OK; result = src.next(name, size)) {
251+
RET_ERROR(ddoc.addFile(name, "application/octet-stream", size, src));
252+
d->files.push_back({name, size_t(size)});
253+
}
254+
if(result != END_OF_STREAM)
255+
return result;
256+
} else {
257+
RET_ERROR(src.next(name, size));
258+
if(auto rv = src.readAll(enc); rv >= 0)
259+
d->files.push_back({std::move(name), size_t(rv)});
260+
else
261+
return rv;
257262
}
258-
if(result != END_OF_STREAM)
259-
return result;
260-
} else {
261-
RET_ERROR(src.next(name, size));
262-
if(auto rv = src.readAll(enc); rv >= 0)
263-
d->files.push_back({std::move(name), size_t(rv)});
264-
else
265-
return rv;
266-
}
267-
RET_ERROR(enc.close());
268-
RET_ERROR(vcons.close());
269-
return d->writeBase64Element(Private::DENC, "CipherValue", data);
263+
return enc.close();
264+
});
270265
}));
271266
RET_ERROR(d->writeEncryptionProperties(use_ddoc));
272267
d.reset();
@@ -318,22 +313,19 @@ CDoc1Writer::finishEncryption()
318313
bool use_ddoc = d->files.size() > 1;
319314
libcdoc::Crypto::Key transportKey = libcdoc::Crypto::generateKey(d->method);
320315

321-
RET_ERROR(d->writeKeyInfo(use_ddoc, transportKey));
316+
RET_ERROR(d->writeKeyInfo(use_ddoc, d->rcpts, transportKey));
322317
RET_ERROR(d->writeElement(Private::DENC, "CipherData", [&] {
323-
std::vector<uint8_t> data;
324-
data.reserve(16384);
325-
VectorConsumer vcons(data);
326-
EncryptionConsumer enc(vcons, d->method, transportKey);
327-
if(use_ddoc)
328-
{
329-
for(DDOCWriter ddoc(enc); const FileEntry& file : d->files)
330-
RET_ERROR(ddoc.addFile(file.name, "application/octet-stream", file.data));
331-
}
332-
else
333-
RET_ERROR(VectorSource(d->files.back().data).readAll(enc));
334-
RET_ERROR(enc.close());
335-
RET_ERROR(vcons.close());
336-
return d->writeBase64Element(Private::DENC, "CipherValue", data);
318+
return d->writeBase64Element(Private::DENC, "CipherValue", [&](DataConsumer &dst) -> int64_t {
319+
EncryptionConsumer enc(dst, d->method, transportKey);
320+
if(use_ddoc)
321+
{
322+
for(DDOCWriter ddoc(enc); const FileEntry& file : d->files)
323+
RET_ERROR(ddoc.addFile(file.name, "application/octet-stream", file.data));
324+
}
325+
else
326+
RET_ERROR(VectorSource(d->files.back().data).readAll(enc));
327+
return enc.close();
328+
});
337329
}));
338330
RET_ERROR(d->writeEncryptionProperties(use_ddoc));
339331
d.reset();

cdoc/Crypto.cpp

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -573,11 +573,11 @@ EncryptionConsumer::write(const uint8_t *src, size_t size)
573573
return OK;
574574
if(error != OK)
575575
return error;
576-
std::vector<uint8_t> data(size + EVP_CIPHER_CTX_block_size(ctx.get()) - 1);
577-
int len = int(data.size());
578-
if(SSL_FAILED(EVP_CipherUpdate(ctx.get(), data.data(), &len, src, int(size)), "EVP_CipherUpdate"))
576+
buf.resize(std::max(buf.size(), size + EVP_CIPHER_CTX_block_size(ctx.get()) - 1));
577+
int len = int(buf.size());
578+
if(SSL_FAILED(EVP_CipherUpdate(ctx.get(), buf.data(), &len, src, int(size)), "EVP_CipherUpdate"))
579579
return CRYPTO_ERROR;
580-
return dst.write(data.data(), size_t(len));
580+
return dst.write(buf.data(), size_t(len));
581581
}
582582

583583
result_t
@@ -592,11 +592,11 @@ EncryptionConsumer::writeAAD(const std::vector<uint8_t> &data)
592592
result_t
593593
EncryptionConsumer::close()
594594
{
595-
std::vector<uint8_t> data(EVP_CIPHER_CTX_block_size(ctx.get()));
596-
int len = int(data.size());
597-
if(SSL_FAILED(EVP_CipherFinal(ctx.get(), data.data(), &len), "EVP_CipherFinal"))
595+
buf.resize(std::max(buf.size(), size_t(EVP_CIPHER_CTX_block_size(ctx.get()))));
596+
int len = int(buf.size());
597+
if(SSL_FAILED(EVP_CipherFinal(ctx.get(), buf.data(), &len), "EVP_CipherFinal"))
598598
return CRYPTO_ERROR;
599-
if(auto rv = dst.write(data.data(), size_t(len)); rv < 0)
599+
if(auto rv = dst.write(buf.data(), size_t(len)); rv < 0)
600600
return rv;
601601
std::array<uint8_t, 16> tag {};
602602
if(EVP_CIPHER_CTX_mode(ctx.get()) == EVP_CIPH_GCM_MODE)

cdoc/Crypto.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ class Crypto
124124

125125
struct EncryptionConsumer final : public DataConsumer {
126126
EncryptionConsumer(DataConsumer &dst, const std::string &method, const Crypto::Key &key);
127-
EncryptionConsumer(DataConsumer &dst, const EVP_CIPHER *cipher, const Crypto::Key &key);
127+
EncryptionConsumer(DataConsumer &dst, const EVP_CIPHER *cipher, const Crypto::Key &key);
128128
CDOC_DISABLE_MOVE_COPY(EncryptionConsumer)
129129
result_t write(const uint8_t *src, size_t size) final;
130130
result_t writeAAD(const std::vector<uint8_t> &data);
@@ -135,6 +135,7 @@ struct EncryptionConsumer final : public DataConsumer {
135135
unique_free_t<EVP_CIPHER_CTX> ctx;
136136
DataConsumer &dst;
137137
result_t error = OK;
138+
std::vector<uint8_t> buf;
138139
};
139140

140141
}; // namespace libcdoc

cdoc/DDocWriter.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
#include "DDocWriter.h"
2020

21+
#include "Io.h"
22+
2123
using namespace libcdoc;
2224

2325
/**
@@ -38,6 +40,24 @@ DDOCWriter::~DDOCWriter() noexcept
3840
writeEndElement(DDOC); // SignedDoc
3941
}
4042

43+
/**
44+
* Add File to container
45+
* @param file Filename
46+
* @param mime File mime type
47+
* @param size File size
48+
* @param data File content
49+
*/
50+
int64_t DDOCWriter::addFile(const std::string &file, const std::string &mime, size_t size, libcdoc::DataSource& src)
51+
{
52+
return writeBase64Element(DDOC, "DataFile", [&src](DataConsumer &dst){ return src.readAll(dst); }, {
53+
{"ContentType", "EMBEDDED_BASE64"},
54+
{"Filename", file},
55+
{"Id", "D" + std::to_string(fileCount++)},
56+
{"MimeType", mime},
57+
{"Size", std::to_string(size)}
58+
});
59+
}
60+
4161
/**
4262
* Add File to container
4363
* @param file Filename

cdoc/DDocWriter.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@
2121
#include "XmlWriter.h"
2222

2323
namespace libcdoc {
24+
struct DataSource;
2425

2526
class DDOCWriter final: public XMLWriter
2627
{
2728
public:
2829
DDOCWriter(DataConsumer &dst);
2930
~DDOCWriter() noexcept final;
3031

32+
int64_t addFile(const std::string &name, const std::string &mime, size_t size, libcdoc::DataSource &src);
3133
int64_t addFile(const std::string &name, const std::string &mime, const std::vector<unsigned char> &data);
3234

3335
private:

cdoc/XmlWriter.cpp

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424

2525
#include <libxml/xmlwriter.h>
2626

27+
#include <array>
28+
2729
using namespace libcdoc;
2830

2931
using pcxmlChar = xmlChar *;
@@ -111,6 +113,66 @@ int64_t XMLWriter::writeElement(const NS &ns, const std::string &name, const std
111113
return writeEndElement(ns);
112114
}
113115

116+
int64_t XMLWriter::writeBase64Element(const NS &ns, const std::string &name, const std::function<int64_t(DataConsumer&)> &f, const std::map<std::string, std::string> &attr)
117+
{
118+
if(auto rv = writeStartElement(ns, name, attr); rv != OK)
119+
return rv;
120+
121+
struct Base64Consumer : public DataConsumer {
122+
xmlTextWriterPtr w;
123+
std::array<uint8_t, 3> buf {}; // buffer up to 2 leftover bytes
124+
size_t bufSize = 0;
125+
Base64Consumer(xmlTextWriterPtr _w) : w(_w) {}
126+
result_t write(const uint8_t *src, size_t size) final {
127+
if(!src || size == 0)
128+
return OK;
129+
130+
size_t pos = 0;
131+
if(bufSize > 0) {
132+
pos = std::min(buf.size() - bufSize, size);
133+
std::copy(src, src + pos, buf.begin() + bufSize);
134+
bufSize += pos;
135+
if (bufSize < 3) {
136+
return result_t(size);
137+
}
138+
if (xmlTextWriterWriteBase64(w, reinterpret_cast<const char*>(buf.data()), 0, buf.size()) == -1)
139+
return IO_ERROR;
140+
bufSize = 0;
141+
}
142+
143+
// Write largest contiguous chunk with length multiple of 3
144+
size_t remaining = size - pos;
145+
if(size_t fullTriples = remaining - (remaining % 3); fullTriples > 0) {
146+
if (xmlTextWriterWriteBase64(w, reinterpret_cast<const char*>(src), pos, fullTriples) == -1)
147+
return IO_ERROR;
148+
pos += fullTriples;
149+
}
150+
151+
// Buffer leftover (0..2) bytes for next write/close
152+
if(bufSize = size - pos; bufSize > 0) {
153+
std::copy(src + pos, src + size, buf.begin());
154+
}
155+
156+
return result_t(size);
157+
}
158+
result_t close() final {
159+
if (bufSize > 0) {
160+
// write remaining 1..2 bytes so base64 padding is applied only at the end
161+
if(xmlTextWriterWriteBase64(w, reinterpret_cast<const char*>(buf.data()), 0, bufSize) == -1)
162+
return IO_ERROR;
163+
}
164+
bufSize = 0;
165+
return OK;
166+
}
167+
bool isError() final { return false; }
168+
} base64Consumer {d->w.get()};
169+
if(auto rv = f(base64Consumer); rv < 0)
170+
return rv;
171+
if(auto rv = base64Consumer.close(); rv < 0)
172+
return rv;
173+
return writeEndElement(ns);
174+
}
175+
114176
int64_t XMLWriter::writeBase64Element(const NS &ns, const std::string &name, const std::vector<xmlChar> &data, const std::map<std::string, std::string> &attr)
115177
{
116178
if(auto rv = writeStartElement(ns, name, attr); rv != OK)

cdoc/XmlWriter.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class XMLWriter
4141
int64_t writeEndElement(const NS &ns);
4242
int64_t writeElement(const NS &ns, const std::string &name, const std::function<int64_t()> &f = nullptr);
4343
int64_t writeElement(const NS &ns, const std::string &name, const std::map<std::string, std::string> &attr, const std::function<int64_t()> &f = nullptr);
44+
int64_t writeBase64Element(const NS &ns, const std::string &name, const std::function<int64_t(DataConsumer &)> &f, const std::map<std::string, std::string> &attr = {});
4445
int64_t writeBase64Element(const NS &ns, const std::string &name, const std::vector<unsigned char> &data, const std::map<std::string, std::string> &attr = {});
4546
int64_t writeTextElement(const NS &ns, const std::string &name, const std::map<std::string, std::string> &attr, const std::string &data);
4647

vcpkg.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"features": {
2222
"tests": { "description": "Build tests", "dependencies": ["boost-test"] }
2323
},
24-
"builtin-baseline": "031ad89ce6c575df35a8e58707ad2c898446c63e",
24+
"builtin-baseline": "085820b35f4ef5ad54967c8a46fb822e53c4be33",
2525
"vcpkg-configuration": {
2626
"overlay-triplets": ["./vcpkg-triplets"]
2727
}

0 commit comments

Comments
 (0)