From 0f8a7f58fef649f77800a7a4b7d307a15ac5f760 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sat, 20 Sep 2025 00:38:27 -0300 Subject: [PATCH 1/3] Add new attachment encrypt/decrypt functions This defines a new determinstic, streamable attachment encryption/decryption for Session clients to use in the future, built on libsodium's crypto_secretstream API (which is XChaCha20-poly1305-based). Unlike current Session attachment encryption, the encryption added here uses deterministic (though private, cryptographically secure) nonce and key generated from the user, file contents itself, and type of file, so that the same user uploading an identical attachment results in an identical encrypted copy, thus allowing deduplication in case of things like file server response timeouts or repeated uploads of the same profile picture. Unlike current attachments, these functions also allow streaming of attachment data (via the added Decryptor) so that, in future clients, an attachment can be decrypted and saved as it arrives, rather than needing to store the whole thing in memory to decrypt it. This also reimplements the padding added to attachments. Currently clients use this monstrosity: let desiredSize: Int = max(541, min(Int(Network.maxFileSize), Int(floor(pow(1.05, ceil(log(Double(plaintext.count)) / log(1.05))))))) which is not only difficult to read, but also involves several floating point functions where imprecision (and thus potential metadata leakage about which client created it) can creep in. The new protocol works vaguely similar: we pad up to fixed sizes that with the size of the attachment. Moreover there is no way to recover the amount of padding from the attachment itself: rather a Session client has to communicate the URL, key, *and size* to someone they are trying to send to out of band, who then downloads, decrypts the whole thing, then chops off the padding based on what they were told the size was. With the new scheme, padding is now deterministic and embedded in the pre-encrypted data, and is transparently stripped out when decrypting. The plan is for Session clients to start understanding (but not using) this new encryption format, and then once sufficient time has passed for clients to have upgraded, start using this instead of the current AES-GCM encryption. --- include/session/attachments.hpp | 208 +++++++++++++ include/session/util.hpp | 10 +- src/CMakeLists.txt | 1 + src/attachments.cpp | 502 ++++++++++++++++++++++++++++++ tests/CMakeLists.txt | 1 + tests/test_attachment_encrypt.cpp | 195 ++++++++++++ 6 files changed, 914 insertions(+), 3 deletions(-) create mode 100644 include/session/attachments.hpp create mode 100644 src/attachments.cpp create mode 100644 tests/test_attachment_encrypt.cpp diff --git a/include/session/attachments.hpp b/include/session/attachments.hpp new file mode 100644 index 00000000..a63c53a4 --- /dev/null +++ b/include/session/attachments.hpp @@ -0,0 +1,208 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +#include "session/sodium_array.hpp" + +namespace session::attachment { + +/// Attachment domain separators, used to differentiate the key/nonce generated for an attachment +/// used for a different purpose. +enum class Domain : uint8_t { + /// Domain for a generic attachment, i.e. a file sent from one user to another: + ATTACHMENT = 0x00, + /// Domain for profile pics: + PROFILE_PIC = 0x01, +}; + +// Size of initial encryption header (== crypto_secretstream_xchacha20poly1305_HEADERBYTES) +constexpr size_t ENCRYPT_HEADER = 24; +// Size of chunks that we encrypt at a time: +constexpr size_t ENCRYPT_CHUNK_SIZE = 32'768; +// The overhead of the mac+tag added to each chunk (== crypto_secretstream_xchacha20poly1305_ABYTES) +constexpr size_t ENCRYPT_CHUNK_OVERHEAD = 17; + +constexpr size_t ENCRYPTED_CHUNK_TOTAL = ENCRYPT_CHUNK_SIZE + ENCRYPT_CHUNK_OVERHEAD; + +// Random encryption key size: (== crypto_secretstream_xchacha20poly1305_KEYBYTES) +constexpr size_t ENCRYPT_KEY_SIZE = 32; + +// The maximum file size that may be encrypted (unless passing the `allow_large` flag). This is the +// maximum size (with a small allowance for padding and request overhead) that can be sent or +// retrieved via oxen-storage-server onion requests, and its padded size is the maximum attachment +// size allowed by the storage server. (Technically this value was chosen as it is the largest +// unencrypted data size that has the same padded+encrypted size as a 10'000'000B file). +constexpr size_t ENCRYPT_MAX_SIZE = + 10218286; // == 10223616 after stream mac+tag and (1-byte) padding + +// Returns the amount of padding to add to an attachment to obfuscate the true size, given +// crypto_secretstream encryption with a 32kiB chunk size. We determine the padded size as follows, +// given an input size N: +// +// - compute the total raw size M as N plus: +// - 1 for the 'S' prefix (outside the encryption) +// - 17 byte encryption overhead (crypto_secretstream_xchacha20poly1305_ABYTES = poly1305 MAC + +// 1-byte tag) for every 32kiB (or piece thereof) +// - 1 byte for the minimum padding size +// +// We then take keep the most-significant bit of M (i.e. reduce to the largest power of 2 <= M), +// right-shift this by 5, and that round up to the next multiple of that padding factor. +// +// For example, for an input of 1MB (N=100000), we have an unpadded total size of 1000000+1+31*17 = +// 1000528 (i.e. accounting for the 'S' identifier, and the 31 mac+tag values). We then obtain the +// highest power of two <= this (524288 = 2^19), right-shift by 5 to get 16384 (2^14), and then +// round up the total size to the next multiple of that: 1015808. Thus we require an additional +// 15280 padding bytes, and so in total we get: +// +// 1 -- the leading (unencrypted) S +// + 15279 × 0x00 -- leading padding bytes +// + 1 × 0x01 -- final padding byte +// + 1000000 -- encrypted file stream data (ignoring embedded mac+tags) +// + 31 × 17 -- embedded mac+tags after every 32kiB of file stream data +// = 1015808 final output. +// +// (Note that we always including at least one padding byte, and there are some complications in the +// calculation as padding values get large enough to start inducing additional mac+tags; see the +// implementation for details). +size_t encrypted_padding(size_t data_size); + +/// API: crypto/attachment::encrypt +/// +/// Encrypt an attachment for storage on the file server and distribution to other users using +/// deterministic encryption where we use a cryptographically secure hash of the sending user's +/// private key and file content to generate the encryption key/nonce pair. The main advantage of +/// this is deduplication: the same attachment uploaded by the same user will result in the same +/// encrypted content, thus allowing deduplication of identical uploads on the file server. This is +/// particularly important for profile pictures, which are frequently re-uploaded to keep the +/// attachment alive. +/// +/// We currently always encrypt in chunks of (max) 32kiB via libsodium's crypto_secretstream API, +/// and prefix the encrypted data with a 0x53 ('S') to indicate this. Any other value of the first +/// byte is reserved for possible alternative future encryption mechanisms. +/// +/// We prepend padding of at least 1 byte before the actual data, by prepending (PADDING-1) 0x00 +/// bytes followed by a single 0x01 byte to the actual data stream; this data is discard when +/// decrypting. +/// +/// Inputs: +/// - `seed` -- the 32-byte seed of the sender; it is recommended that this be the 32-byte Session +/// seed so that the same Session ID always uses the same base seed, but any 32-byte value can be +/// passed (i.e. it is not strictly required that it be a Session seed). Only the first 32 bytes +/// of longer values will be used (and thus passing the 64-byte seed+pubkey libsodium full secret +/// is equivalent to passing just the seed). +/// +/// - `data` -- the buffer of data to encrypt. +/// +/// - `domain` -- domain separator; uploads of funamentally different types should use a different +/// value, so that an identical upload used for different purposes will have unrelated key/nonce +/// values. +/// +/// - `allow_large` -- defaults to false; if true, this function will accept an input larger value +/// than MAX_REGULAR_SIZE. This option should only be passed when compatibility with onion +/// requests is not needed. +/// +/// Outputs: +/// - Pair of values: the padded+encrypted data, and the decryption key (32 bytes), both in raw +/// bytes. +/// +/// Throws std::invalid_argument if `seed` is shorter than 32 bytes, or if data is larger than +/// MAX_REGULAR_SIZE (unless `allow_large` is true). +/// +std::pair, std::array> encrypt( + std::span seed, + std::span data, + Domain domain, + bool allow_large = false); + +/// API: crypto/attachment::decrypt +/// +/// Decrypts an attachment allegedly produced by attachment::encrypt to a single in-memory buffer. +/// +/// Inputs: +/// - `data` -- in-memory buffer of data to decrypt. +/// - `key` -- the 32-byte decryption key +/// +/// Outputs: +/// - std::vector of decrypted, de-padded data. +/// +/// Throws std::runtime_error if decryption fails. +std::vector decrypt( + std::span encrypted, std::span key); + +/// API: crypto/attachment::Decryptor +/// +/// Object-based interfaced to streaming decryption. The basic usage is to construct the object +/// with an output callback, then repeatedly feed it any amount of additional data via `update()` +/// until all data has been provided. Calls to `update()` will invoke the output callback as soon +/// as enough data has been provided to advance to the next stream chunk(s), and so one call to +/// update() could result in any number of calls to output(), including 0. Once all data has been +/// provided, `finalize()` is called to signal the end of the input data. +/// +/// If a problem with the data is found, the `update()` or `finalize()` call will returns false +/// indicating that the decryption failed, and any partially decrypted output data provided to the +/// output callback should be discarded or deleted. Further calls to `update()` or `finalize()` +/// after such a failure will simply return false without processing any additional data. +/// +/// This class is not recommended if the intention is to build an in-memory buffer from existing +/// in-memory data: `decrypt()` will be more memory efficient in that case. +class Decryptor { + std::function decrypted)> output; + std::vector buf; + bool header = false; + bool depadded = false; + bool failed = false; + bool finished = false; + bool hit_final = false; + cleared_uc32 key; + unsigned char st_data[52]; // crypto_secretstream_xchacha20poly1305_state data + + void process_header(std::span chunk); + void process_chunk(std::span chunk, bool is_final = false); + + public: + /// Constructs a decryptor. The given output will be called as soon as enough data has been + /// accumulated to validate additional decrypted data. + Decryptor( + std::span key, + std::function decrypted)> output); + + /// Provides more data to the decryptor. If the additional data completes an input data chunk + /// then output will be called with the partially decrypted data. Returns true if the data was + /// accepted, false if data stream decryption failed (either because of the new data, or some + /// previous failure). + /// + /// Throws std::logic_error if called after a successful finalize(). + bool update(std::span enc_data); + + /// Called to signal the end of the encrypted data stream. If all data was processed + /// successfully and the stream ended properly, this returns true; returns false if the stream + /// data did not indicate finality (or if a previous update returned failure). + /// + /// Throws std::logic_error if called after a successful finalize(). + bool finalize(); +}; + +/// API: crypto/attachment::decrypt +/// +/// Decrypts an attachment allegedly produced by attachment::encrypt to an output file. Overwrites +/// the file if it already exists. +/// +/// Inputs: +/// - `data` -- in-memory buffer of data to decrypt. +/// - `key` -- the 32-byte decryption key. +/// - `filename` -- where to write the output file. +/// +/// Outputs: None. +/// +/// Throws std::runtime_error if decryption fails or if writing to the file fails. Upon exception a +/// partially written file will be deleted. +void decrypt( + std::span encrypted, + std::span key, + const std::filesystem::path& filename); + +} // namespace session::attachment diff --git a/include/session/util.hpp b/include/session/util.hpp index fc57908f..8ff560a4 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -21,9 +21,13 @@ namespace session { using namespace oxenc; // Helper functions to convert to/from spans -template -inline std::span as_span(const std::span& sp) { - return {reinterpret_cast(sp.data()), sp.size()}; +template +inline std::span as_span(std::span sp) { + return std::span{reinterpret_cast(sp.data()), sp.size()}; +} +template +inline std::span as_span(std::span sp) { + return std::span{reinterpret_cast(sp.data()), sp.size()}; } template diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1e4ad566..7b4e66b7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -43,6 +43,7 @@ add_libsession_util_library(util ) add_libsession_util_library(crypto + attachments.cpp blinding.cpp curve25519.cpp ed25519.cpp diff --git a/src/attachments.cpp b/src/attachments.cpp new file mode 100644 index 00000000..269a3968 --- /dev/null +++ b/src/attachments.cpp @@ -0,0 +1,502 @@ +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace session::attachment { + +using namespace oxen::log::literals; + +static_assert(ENCRYPT_HEADER == crypto_secretstream_xchacha20poly1305_HEADERBYTES); +static_assert(ENCRYPT_CHUNK_OVERHEAD == crypto_secretstream_xchacha20poly1305_ABYTES); +static_assert(ENCRYPT_KEY_SIZE == crypto_secretstream_xchacha20poly1305_KEYBYTES); + +size_t encrypted_padding(size_t data_size) { + constexpr size_t prefix_size = 1 + ENCRYPT_HEADER; + constexpr size_t min_padding = 1; + + // the number of mac+tag values embedded every 32kiB in the data stream: + const size_t stream_chunks = (data_size + ENCRYPT_CHUNK_SIZE - 1) / ENCRYPT_CHUNK_SIZE; + + const size_t enc_size = + data_size + prefix_size + min_padding + stream_chunks * ENCRYPT_CHUNK_OVERHEAD; + + const size_t pad_factor = std::bit_floor(std::max(enc_size, 131072)) >> 5; + + // Round up to next multiple of pad_factor: + const size_t padded_size = (enc_size + pad_factor - 1) / pad_factor * pad_factor; + + size_t padding = padded_size - enc_size + min_padding; + + // For every complete ENCRYPT_CHUNK_SIZE padding that we add we implicitly also add a stream + // tag, and so we want to subtract one tag per (ENCRYPT_CHUNK_SIZE+ENCRYPT_CHUNK_OVERHEAD) bytes + // of padding to compensate (so that the added tag gets counted as an implicit part of the + // padding): + size_t implicit_padding = 0; + if (padding >= ENCRYPTED_CHUNK_TOTAL) + implicit_padding = (padding / ENCRYPTED_CHUNK_TOTAL) * ENCRYPT_CHUNK_OVERHEAD; + + // After accounting for the full stream + tag blocks above, we might still have enough to spill + // over the stream into the next chunk, and so if that is going to happen, we want to count the + // implied additional tag as part of the padding as well. + + // This is how much padding we can add without spilling over into a new stream chunk: + const size_t free_padding = stream_chunks * ENCRYPT_CHUNK_SIZE - data_size; + + if (padding % ENCRYPTED_CHUNK_TOTAL > free_padding) + implicit_padding += ENCRYPT_CHUNK_OVERHEAD; + + padding -= implicit_padding; + + return padding; +} + +// We have to roll our own custom version of crypto_secretstream_xchacha20poly1305_init_push here +// because libsodium offers no way to provide the randomness it uses (it hard codes a call to +// randombytes_buf), and so this repeats its internal implementation but using our hashed data for +// the randomness. +static crypto_secretstream_xchacha20poly1305_state +secretstream_xchacha20poly1305_init_push_with_nonce( + std::span header, + std::span key, + std::span nonce) { + + crypto_secretstream_xchacha20poly1305_state st; + + std::memcpy(header.data(), nonce.data(), ENCRYPT_HEADER); + crypto_core_hchacha20( + st.k, header.data(), reinterpret_cast(key.data()), nullptr); + static_assert(sizeof(st) == 52); + std::memset(st.nonce, 0, 4 /*crypto_secretstream_xchacha20poly1305_COUNTERBYTES*/); + st.nonce[0] = 1; + std::memcpy( + st.nonce + 4 /*crypto_secretstream_xchacha20poly1305_COUNTERBYTES*/, + header.data() + crypto_core_hchacha20_INPUTBYTES, + 8 /*crypto_secretstream_xchacha20poly1305_INONCEBYTES*/); + std::memset(st._pad, 0, sizeof(st._pad)); + + return st; +} + +std::pair, std::array> encrypt( + std::span seed, + std::span data, + Domain domain, + bool allow_large) { + + if (seed.size() < 32) + throw std::invalid_argument{"attachment::encrypt requires a 32-byte uploader seed"}; + + if (data.size() > ENCRYPT_MAX_SIZE && !allow_large) + throw std::invalid_argument{"data to encrypt is too large"}; + + std::pair, std::array> result; + auto& [out, key] = result; + + std::span udata{ + reinterpret_cast(data.data()), data.size()}; + + std::array nonce_key; + + crypto_generichash_blake2b_state b_st; + const auto domain_byte = static_cast(domain); + crypto_generichash_blake2b_init(&b_st, &domain_byte, 1, nonce_key.size()); + crypto_generichash_blake2b_update( + &b_st, reinterpret_cast(seed.data()), 32); + crypto_generichash_blake2b_update(&b_st, udata.data(), udata.size()); + crypto_generichash_blake2b_final(&b_st, nonce_key.data(), nonce_key.size()); + std::memcpy(key.data(), nonce_key.data() + ENCRYPT_HEADER, ENCRYPT_KEY_SIZE); + + size_t padding = encrypted_padding(data.size()); + assert(padding >= 1); + + size_t padded_size = data.size() + padding; + size_t tags_size = + (padded_size + ENCRYPT_CHUNK_SIZE - 1) / ENCRYPT_CHUNK_SIZE * ENCRYPT_CHUNK_OVERHEAD; + + out.resize(1 + ENCRYPT_HEADER + data.size() + padding + tags_size); + out[0] = std::byte{'S'}; + + std::span uout{reinterpret_cast(out.data()), out.size()}; + + std::span header{uout.data() + 1, ENCRYPT_HEADER}; + + auto st = secretstream_xchacha20poly1305_init_push_with_nonce( + header, as_span(std::span{key}), std::span{nonce_key}.first()); + + auto* outpos = uout.data() + 1 + ENCRYPT_HEADER; + auto* const outend = uout.data() + uout.size(); + auto* inpos = udata.data(); + auto* const inend = inpos + udata.size(); + + // Now we build a buffer containing padding, plus whatever initial actual data goes on the end + // of the last chunk of padding: + { + std::vector buf; + buf.reserve(std::min(ENCRYPT_CHUNK_SIZE, padded_size)); + for (size_t padding_remaining = padding; padding_remaining;) { + if (padding_remaining > ENCRYPT_CHUNK_SIZE) { + // Full chunk of 0x00 padding (with more padding in the next chunk) + buf.resize(ENCRYPT_CHUNK_SIZE); + padding_remaining -= ENCRYPT_CHUNK_SIZE; + } else { + buf.resize(padding_remaining - 1); // 0x00 padding + buf.push_back(0x01); // padding terminator + if (size_t first_data = + std::min(ENCRYPT_CHUNK_SIZE - padding_remaining, udata.size())) { + buf.insert(buf.end(), inpos, inpos + first_data); + inpos += first_data; + } + padding_remaining = 0; + } + + assert(outpos + buf.size() + crypto_secretstream_xchacha20poly1305_ABYTES <= outend); + + unsigned char tag = inpos < inend ? 0 : crypto_secretstream_xchacha20poly1305_TAG_FINAL; + + unsigned long long out_len; + crypto_secretstream_xchacha20poly1305_push( + &st, outpos, &out_len, buf.data(), buf.size(), nullptr, 0, tag); + assert(out_len == buf.size() + crypto_secretstream_xchacha20poly1305_ABYTES); + outpos += out_len; + } + } + + // Now we're through the initial padding (and probably some initial data): now all we need to do + // is push the rest of the data + while (inpos < inend) { + auto* chunk_start = inpos; + inpos = std::min(chunk_start + ENCRYPT_CHUNK_SIZE, inend); + assert(outpos + (inpos - chunk_start) + crypto_secretstream_xchacha20poly1305_ABYTES <= + outend); + + unsigned char tag = inpos < inend ? 0 : crypto_secretstream_xchacha20poly1305_TAG_FINAL; + + unsigned long long out_len; + crypto_secretstream_xchacha20poly1305_push( + &st, outpos, &out_len, chunk_start, inpos - chunk_start, nullptr, 0, tag); + assert(out_len == inpos - chunk_start + crypto_secretstream_xchacha20poly1305_ABYTES); + outpos += out_len; + } + + return result; +} + +std::vector decrypt( + std::span encrypted, std::span key) { + + if (encrypted.size() <= 1 + ENCRYPT_HEADER + ENCRYPT_CHUNK_OVERHEAD) + throw std::runtime_error{"Attachment decryption failed: encrypted data too short"}; + + if (encrypted.front() != std::byte{'S'}) + throw std::runtime_error{ + "Attachment decryption failed: unknown encryption type 0x{:02x}; expected 0x53 (S)"_format( + +static_cast(encrypted.front()))}; + + std::span uenc{ + reinterpret_cast(encrypted.data()), encrypted.size()}; + + auto header = uenc.subspan<1, ENCRYPT_HEADER>(); + + crypto_secretstream_xchacha20poly1305_state st; + crypto_secretstream_xchacha20poly1305_init_pull( + &st, uenc.data() + 1, reinterpret_cast(key.data())); + + auto* inpos = uenc.data() + 1 + ENCRYPT_HEADER; + auto* const inend = uenc.data() + uenc.size(); + + std::vector decrypted; + bool done = false; + + // Discard any leading padding chunks (of which there is *always* at least 1 because we always + // have at least one byte of padding, even for an empty file). The last such chunk will + // typically have the beginning of the actual data. Once we figure out how much padding there + // is we can calculate the remaining data and reserve the output buffer. + { + std::vector padbuf; + padbuf.reserve(std::min(inend - inpos - ENCRYPT_CHUNK_OVERHEAD, ENCRYPT_CHUNK_SIZE)); + do { + if (inpos + ENCRYPT_CHUNK_OVERHEAD >= inend) + throw std::runtime_error{ + "Attachment decryption failed: data ended in the middle of padding"}; + + size_t chunk_size = + std::min(inend - inpos - ENCRYPT_CHUNK_OVERHEAD, ENCRYPT_CHUNK_SIZE); + padbuf.resize(chunk_size); + + unsigned char tag; + if (crypto_secretstream_xchacha20poly1305_pull( + &st, + reinterpret_cast(padbuf.data()), + nullptr, + &tag, + inpos, + chunk_size + ENCRYPT_CHUNK_OVERHEAD, + nullptr, + 0) != 0) + throw std::runtime_error{ + "Attachment decryption failed: invalid key or corrupted data"}; + + inpos += chunk_size + ENCRYPT_CHUNK_OVERHEAD; + + auto padend = std::find_if_not(padbuf.begin(), padbuf.end(), [](const std::byte c) { + return c == std::byte{0x00}; + }); + if (padend != padbuf.end()) { + if (*padend != std::byte{0x01}) + throw std::runtime_error{"Attachment decryption failed: invalid padding"}; + ++padend; + + std::span init_data{padend, padbuf.end()}; + // We've identified the start of the data: assuming it is valid, the remaining of + // the encrypted data consists of N chunks of + // (ENCRYPT_CHUNK_SIZE+ENCRYPT_CHUNK_OVERHEAD) full data chunks plus one final + // (chunk+ENCRYPT_CHUNK_OVERHEAD). + size_t final_size = init_data.size() + (inend - inpos) - + (inend - inpos + ENCRYPTED_CHUNK_TOTAL - 1) / + ENCRYPTED_CHUNK_TOTAL * ENCRYPT_CHUNK_OVERHEAD; + decrypted.reserve(final_size); + decrypted.insert(decrypted.end(), padend, padbuf.end()); + + if (tag == crypto_secretstream_xchacha20poly1305_TAG_FINAL) { + if (inpos != inend) + throw std::runtime_error{ + "Attachment decryption failed: FINAL tag before end of the " + "encrypted data"}; + done = true; + } else if ( + inpos == inend && tag != crypto_secretstream_xchacha20poly1305_TAG_FINAL) { + throw std::runtime_error{ + "Attachment decryption failed: end of data without FINAL tag"}; + } + + break; + } + } while (true); + } + + while (!done) { + if (inpos + ENCRYPT_CHUNK_OVERHEAD >= inend) + throw std::runtime_error{ + "Attachment decryption failed: data ended before end of stream"}; + + size_t chunk_size = std::min(inend - inpos - ENCRYPT_CHUNK_OVERHEAD, ENCRYPT_CHUNK_SIZE); + assert(decrypted.capacity() >= decrypted.size() + chunk_size); + decrypted.resize(decrypted.size() + chunk_size); + auto* out = + reinterpret_cast(decrypted.data() + decrypted.size() - chunk_size); + + unsigned char tag; + if (crypto_secretstream_xchacha20poly1305_pull( + &st, + out, + nullptr, + &tag, + inpos, + chunk_size + ENCRYPT_CHUNK_OVERHEAD, + nullptr, + 0) != 0) + throw std::runtime_error{"Attachment decryption failed: invalid key or corrupted data"}; + + inpos += chunk_size + ENCRYPT_CHUNK_OVERHEAD; + + if (tag == crypto_secretstream_xchacha20poly1305_TAG_FINAL) { + if (inpos != inend) + throw std::runtime_error{ + "Attachment decryption failed: FINAL tag before end of the " + "encrypted data"}; + done = true; + } else if (inpos == inend && tag != crypto_secretstream_xchacha20poly1305_TAG_FINAL) { + throw std::runtime_error{"Attachment decryption failed: end of data without FINAL tag"}; + } + } + + return decrypted; +} + +Decryptor::Decryptor( + std::span key_, + std::function decrypted)> output_) : + output{std::move(output_)} { + + std::memcpy(key.data(), key_.data(), key.size()); + + static_assert( + sizeof(crypto_secretstream_xchacha20poly1305_state) == sizeof(Decryptor::st_data)); + static_assert(alignof(crypto_secretstream_xchacha20poly1305_state) == 1); + static_assert(std::is_trivially_copyable_v); +} + +static crypto_secretstream_xchacha20poly1305_state* st(unsigned char* st_data) { + return reinterpret_cast(st_data); +} +void Decryptor::process_header(std::span hdr) { + assert(!header); + + if (hdr[0] != std::byte{'S'}) { + failed = true; + return; + } + + crypto_secretstream_xchacha20poly1305_init_pull( + st(st_data), + reinterpret_cast(hdr.data() + 1), + reinterpret_cast(key.data())); + header = true; +} + +void Decryptor::process_chunk(std::span chunk, bool is_final) { + if (hit_final) { + failed = true; + return; + } + assert(is_final || chunk.size() == ENCRYPTED_CHUNK_TOTAL); + assert(chunk.size() <= ENCRYPTED_CHUNK_TOTAL); + if (chunk.size() < ENCRYPT_CHUNK_OVERHEAD) { + failed = true; + return; + } + + unsigned char tag; + std::array outa; + std::span out{outa.data(), chunk.size() - ENCRYPT_CHUNK_OVERHEAD}; + if (crypto_secretstream_xchacha20poly1305_pull( + st(st_data), + reinterpret_cast(out.data()), + nullptr, + &tag, + reinterpret_cast(chunk.data()), + chunk.size(), + nullptr, + 0) != 0) { + failed = true; + return; + } + + if (tag == crypto_secretstream_xchacha20poly1305_TAG_FINAL) + hit_final = true; + + if (!depadded) { + auto padend = std::find_if_not( + out.begin(), out.end(), [](const std::byte c) { return c == std::byte{0x00}; }); + if (padend != out.end()) { + if (*padend != std::byte{0x01}) { + failed = true; + return; + } + depadded = true; + if (++padend != out.end()) + output(std::span{padend, out.end()}); + } + return; + } + + output(out); +} + +bool Decryptor::update(std::span enc_data) { + if (failed) + return false; + if (finished) + throw std::logic_error{"cannot call update after finalize()"}; + + if (!buf.empty()) { + auto buf_steal = [this, &enc_data](size_t target_buf_size) { + assert(buf.size() < target_buf_size); + size_t steal = std::min(target_buf_size - buf.size(), enc_data.size()); + buf.insert(buf.end(), enc_data.begin(), enc_data.begin() + steal); + enc_data = enc_data.subspan(steal); + assert(buf.size() <= target_buf_size); + return buf.size() == target_buf_size; + }; + + if (!header) { + if (!buf_steal(1 + ENCRYPT_HEADER)) + return true; + process_header(std::span{buf}.first<1 + ENCRYPT_HEADER>()); + } else { + if (!buf_steal(ENCRYPTED_CHUNK_TOTAL)) + return true; + process_chunk(std::span{buf}.first()); + } + buf.clear(); + if (failed) + return false; + } + + if (!header) { + if (enc_data.size() >= 1 + ENCRYPT_HEADER) { + process_header(enc_data.first<1 + ENCRYPT_HEADER>()); + if (failed) + return false; + enc_data = enc_data.subspan(1 + ENCRYPT_HEADER); + } else { + buf.assign(enc_data.begin(), enc_data.end()); + return true; + } + } + + while (enc_data.size() >= ENCRYPTED_CHUNK_TOTAL) { + process_chunk(enc_data.first()); + if (failed) + return false; + enc_data = enc_data.subspan(ENCRYPTED_CHUNK_TOTAL); + } + + if (!enc_data.empty()) + buf.assign(enc_data.begin(), enc_data.end()); + + return true; +} + +bool Decryptor::finalize() { + if (failed) + return false; + + if (!buf.empty()) { + process_chunk(buf, true); + buf.clear(); + } + + if (failed) + return false; + + if (!hit_final) { + failed = true; + return false; + } + + return true; +} + +void decrypt( + std::span encrypted, + std::span key, + const std::filesystem::path& filename) { + + try { + std::ofstream out; + out.exceptions(std::ios::failbit | std::ios::badbit); + out.open(filename, std::ios::binary | std::ios::out | std::ios::trunc); + Decryptor d{key, [&out](std::span data) { + out.write(reinterpret_cast(data.data()), data.size()); + }}; + + d.update(encrypted); + d.finalize(); + } catch (const std::exception& e) { + std::error_code ec; + std::filesystem::remove(filename, ec); + throw; + } +} + +} // namespace session::attachment diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b436795f..77afe5c2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -5,6 +5,7 @@ if(CMAKE_BUILD_TYPE STREQUAL "Release") endif() set(LIB_SESSION_UTESTS_SOURCES + test_attachment_encrypt.cpp test_blinding.cpp test_bt_merge.cpp test_bugs.cpp diff --git a/tests/test_attachment_encrypt.cpp b/tests/test_attachment_encrypt.cpp new file mode 100644 index 00000000..1f27485d --- /dev/null +++ b/tests/test_attachment_encrypt.cpp @@ -0,0 +1,195 @@ +#include + +#include +#include +#include +#include + +#include "utils.hpp" + +using namespace session::config; + +namespace attachment = session::attachment; + +static std::vector make_data(size_t len) { + std::vector v; + v.reserve(len); + for (int i = 0; i < len; i++) + v.push_back(static_cast(i * 7 % 256)); + return v; +} + +using Catch::Matchers::Message; + +TEST_CASE("Attachment encryption/decryption", "[attachments]") { + + auto DATA_SIZE = GENERATE( + 0, + 1, + 2, + 10, + 100, + 1000, + 2000, + 4000, + 4053, + 4054, + 8149, + 8150, + 33333, + 261982, + 261983, + 523990, + 523991, + 6543210, + 10218286); + + auto expected_size = DATA_SIZE < 4054 ? 4096 + : DATA_SIZE < 8150 ? 8192 + : DATA_SIZE < 10000 ? 12288 + : DATA_SIZE == 33333 ? 36864 + : DATA_SIZE < 261983 ? 262144 + : DATA_SIZE < 262000 ? 270336 + : DATA_SIZE < 523991 ? 524288 + : DATA_SIZE < 524000 ? 540672 + : DATA_SIZE == 6543210 ? 6553600 + : DATA_SIZE == 10218286 ? 10223616 + : -1; + + auto seed = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; + + const auto data = make_data(DATA_SIZE); + + auto [enc, key] = attachment::encrypt(seed, data, attachment::Domain::ATTACHMENT); + + auto [enc2, key2] = attachment::encrypt(seed, data, attachment::Domain::ATTACHMENT); + + CHECK(oxenc::to_hex(key) == oxenc::to_hex(key2)); + CHECK(enc.size() == expected_size); + CHECK(!!(enc == enc2)); // Prevent catch2 from trying to expand this on failure + + auto decr = attachment::decrypt(enc, key); + CHECK(decr == data); +} + +TEST_CASE("Attachment encryption/decryption -- large files", "[attachments][large]") { + + auto DATA_SIZE = GENERATE(0, 60'000, 10'000'000, 25'000'000); + + auto expected_size = DATA_SIZE == 0 ? 4096 + : DATA_SIZE == 60000 ? 61440 + : DATA_SIZE == 10000000 ? 10223616 + : DATA_SIZE == 25000000 ? 25165824 + : -1; + + auto seed = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; + + std::vector data; + data.reserve(DATA_SIZE); + for (int i = 0; i < DATA_SIZE; i++) + data.push_back(static_cast(i * 7 % 256)); + + std::vector enc; + std::array key; + if (DATA_SIZE > 10'000'000) { + CHECK_THROWS_MATCHES( + std::tie(enc, key) = + attachment::encrypt(seed, data, attachment::Domain::ATTACHMENT), + std::invalid_argument, + Message("data to encrypt is too large")); + } + std::tie(enc, key) = attachment::encrypt(seed, data, attachment::Domain::ATTACHMENT, true); + + CHECK(enc.size() == expected_size); + + auto decr = attachment::decrypt(enc, key); + CHECK(!!(decr == data)); +} + +const auto bad_data_message = + Message("Attachment decryption failed: invalid key or corrupted data"); + +TEST_CASE("Attachment encryption/decryption -- key separation", "[attachments][key-sep]") { + + auto DATA_SIZE = GENERATE(0, 20, 100, 1000, 33333); + + auto seed = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; + auto seed2 = "8123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; + + const auto data = make_data(DATA_SIZE); + + auto [enc, key] = attachment::encrypt(seed, data, attachment::Domain::ATTACHMENT); + auto [enc2, key2] = attachment::encrypt(seed2, data, attachment::Domain::ATTACHMENT); + + CHECK(oxenc::to_hex(key) != oxenc::to_hex(key2)); + CHECK(!(enc == enc2)); + + CHECK_THROWS_MATCHES(attachment::decrypt(enc, key2), std::runtime_error, bad_data_message); + CHECK_THROWS_MATCHES(attachment::decrypt(enc2, key), std::runtime_error, bad_data_message); +} + +TEST_CASE("Attachment encryption/decryption -- key separation", "[attachments][domain-sep]") { + + auto DATA_SIZE = GENERATE(0, 20, 100, 1000, 33333); + + auto seed = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; + + const auto data = make_data(DATA_SIZE); + + auto [enc, key] = attachment::encrypt(seed, data, attachment::Domain::ATTACHMENT); + auto [enc2, key2] = attachment::encrypt(seed, data, attachment::Domain::PROFILE_PIC); + + CHECK(oxenc::to_hex(key) != oxenc::to_hex(key2)); + CHECK(!(enc == enc2)); + + CHECK_THROWS_MATCHES(attachment::decrypt(enc, key2), std::runtime_error, bad_data_message); + CHECK_THROWS_MATCHES(attachment::decrypt(enc2, key), std::runtime_error, bad_data_message); +} + +TEST_CASE("Attachment encryption/decryption -- content separation", "[attachments][content-sep]") { + + auto seed = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; + + const auto data = make_data(50000); + auto data2 = data; + data2[43210] = std::byte{0x42}; + + auto [enc, key] = attachment::encrypt(seed, data, attachment::Domain::ATTACHMENT); + auto [enc2, key2] = attachment::encrypt(seed, data2, attachment::Domain::ATTACHMENT); + + CHECK(oxenc::to_hex(key) != oxenc::to_hex(key2)); + CHECK(enc.size() == enc2.size()); + CHECK(!(enc == enc2)); + + CHECK_THROWS_MATCHES(attachment::decrypt(enc, key2), std::runtime_error, bad_data_message); + CHECK_THROWS_MATCHES(attachment::decrypt(enc2, key), std::runtime_error, bad_data_message); +} + +TEST_CASE("Attachment Decryptor", "[attachments][decryptor]") { + + auto DATA_SIZE = GENERATE( + 0, 1, 2, 10, 100, 1000, 2000, 4000, 4053, 4054, 8149, 8150, 33333, 6543210, 10218286); + + auto FEED_SIZE = GENERATE(1, 2, 41, 4096, 10000000000); + + auto seed = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; + + const auto data = make_data(DATA_SIZE); + + auto [enc, key] = attachment::encrypt(seed, data, attachment::Domain::ATTACHMENT); + + std::vector decrypted; + attachment::Decryptor d{key, [&decrypted](std::span data) { + decrypted.insert(decrypted.end(), data.begin(), data.end()); + }}; + + std::span input{enc}; + while (!input.empty()) { + auto sz = std::min(FEED_SIZE, input.size()); + REQUIRE(d.update(input.first(sz))); + input = input.subspan(sz); + } + + REQUIRE(d.finalize()); + CHECK(!!(decrypted == data)); +} From aa32593d2ca24661addc4e88ac90f65fe3f30b31 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 23 Sep 2025 23:44:29 -0300 Subject: [PATCH 2/3] Add various buffer<->file encryption/decryption; C API --- include/session/attachments.h | 250 +++++++++++ include/session/attachments.hpp | 208 ++++++++- src/attachments.cpp | 704 +++++++++++++++++++++++++++--- src/internal-util.hpp | 22 + src/session_network.cpp | 12 +- tests/test_attachment_encrypt.cpp | 177 +++++++- 6 files changed, 1283 insertions(+), 90 deletions(-) create mode 100644 include/session/attachments.h create mode 100644 src/internal-util.hpp diff --git a/include/session/attachments.h b/include/session/attachments.h new file mode 100644 index 00000000..1dc3b228 --- /dev/null +++ b/include/session/attachments.h @@ -0,0 +1,250 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include "export.h" + +typedef enum ATTACHMENT_DOMAIN { + ATTACHMENT_DOMAIN_ATTACHMENT = 0x00, + ATTACHMENT_DOMAIN_PROFILE_PIC = 0x01, +} ATTACHMENT_DOMAIN; + +// The size of the encryption key, which is always 32 bytes. +extern const size_t ATTACHMENT_ENCRYPT_KEY_SIZE; + +// The maximum allowed size of an regular attachment that might be sent or retrieved via onion +// requests. Larger attachments are permitted, but will be too large (after padding+encryption) for +// transmission via onion requests. +extern const size_t ATTACHMENT_MAX_REGULAR_SIZE; // 10218286 input == 10223616 encrypted + +/// API: crypto/session_attachment_encrypted_size +/// +/// Returns the exact encrypted+padded output size of an input of `plaintext_size` bytes. This is +/// the size of buffer that must be preallocated for the session_attachment_encrypt functions. +LIBSESSION_EXPORT size_t session_attachment_encrypted_size(size_t plaintext_size); + +/// API: crypto/session_attachment_decrypted_max_size +/// +/// Returns the maximum possible size of the plaintext data given encrypted data of the given size. +/// The actual decrypted size can be smaller, depending on the amount of padding in the encrypted +/// attachment. Returns `(size_t)-1` if the given encrypted size is too small to be a valid +/// encrypted attachment. +LIBSESSION_EXPORT size_t session_attachment_decrypted_max_size(size_t encrypted_size); + +/// API: crypto/session_attachment_encrypt +/// +/// Encrypt an attachment for storage on the file server into a preallocated buffer. +/// +/// Inputs: +/// - `seed` -- the 32-byte unique sender data; typically simply the sender's Ed25519 seed. +/// - `data` -- pointer to the buffer of data to encrypt +/// - `datalen` -- length of the data to encrypt +/// - `domain` -- domain separator; should be an ATTACHMENT_DOMAIN value. +/// - `key_out` -- Pointer to an existing 32-byte buffer where the 32-byte binary decryption key +/// will be written. +/// - `out` -- Pointer to an output buffer, which must be able to contain exactly +/// `session_attachment_encrypted_size(datalen)` encrypted output bytes. +/// - `error` -- if non-NULL and encryption fails then a reason for the error is written here; must +/// be a buffer of at least 256 bytes (or NULL). +/// +/// This method always passes `allow_large` to the underlying C++ implementation; the caller should +/// ensure that the input is less than `ATTACHMENT_MAX_REGULAR_SIZE` bytes before calling this (if +/// compatibility with onion requests is needed). +/// +/// Outputs: +/// - returns the number of encrypted data bytes written to `out` on success, 0 if encryption fails +/// (in which case `error` will be written with the error reason, if non-NULL). +LIBSESSION_EXPORT bool session_attachment_encrypt( + const unsigned char* seed, + const unsigned char* data, + size_t datalen, + ATTACHMENT_DOMAIN domain, + unsigned char* key_out, + unsigned char* out, + char* error); + +/// API: crypto/session_attachment_encrypt_file +/// +/// Encrypt an attachment for storage on the file server from a plaintext file on disk into a +/// preallocated buffer. +/// +/// Note that this implementation needs to read the file *twice*: a first pass to obtain the +/// file-dependent encryption and nonce; and then a second pass to perform the actual encryption, +/// but unlike first reading into a memory buffer, does not have to store the file contents in +/// memory. +/// +/// Inputs: +/// - `seed` -- the 32-byte unique sender data; typically simply the sender's Ed25519 seed. +/// - `filename` -- the filename to read (null-terminated C string). +/// - `domain` -- domain separator; should be an ATTACHMENT_DOMAIN value. +/// - `key_out` -- Pointer to an existing 32-byte buffer where the 32-byte binary decryption key +/// will be written. +/// - `make_buffer` -- callback to invoke to allocate a buffer of the required size into which to +/// write the encrypted data. It is passed two arguments: the required output buffer size, and +/// `ctx`, and must return a pointer to the beginning of a buffer of the requested size in which +/// to write the encrypted data. Can return NULL to abort encryption (e.g. if the size is too +/// large). +/// - `ctx` - arbitrary pointer (which can be null) to pass to make_buffer to pass user-defined +/// context into the callback. This encrypt function does not otherwise touch the pointer. +/// - `error` -- if non-NULL and encryption fails then a reason for the error is written here; must +/// be a buffer of at least 256 bytes (or NULL). +/// +/// This method always passes `allow_large` to the underlying C++ implementation; the caller should +/// ensure that the input is less than `ATTACHMENT_MAX_REGULAR_SIZE` bytes before calling this (if +/// compatibility with onion requests is needed). +/// +/// Outputs: +/// - returns the number of encrypted data bytes written (which will always equal the value passed +/// into `make_buffer`) on success; 0 if encryption fails (in which case `error` will be written +/// with the error reason, if non-NULL). +LIBSESSION_EXPORT size_t session_attachment_encrypt_file( + const unsigned char* seed, + const char* filename, + ATTACHMENT_DOMAIN domain, + unsigned char* key_out, + unsigned char* (*make_buffer)(size_t, void* ctx), + void* ctx, + char* error); + +/// API: crypto/session_attachment_decrypt +/// +/// Decrypts an attachment allegedly produced by session_attachment_encrypt into a provided +/// in-memory buffer. +/// +/// Inputs: +/// - `data` -- pointer to the buffer of data to decrypt +/// - `datalen` -- length of the `data` +/// - `key` -- pointer to the 32-byte binary decryption key +/// - `out` -- output buffer pointer; this buffer must be able to accept the entire decrypted file, +/// which can be anything up to `session_attachment_decrypted_max_size(datalen)` bytes. Note that +/// this buffer may be partially overwritten even if the function returns false (for cases when +/// the decryption error happens later in the encrypted stream). +/// - `outlen` -- pointer in which to store the final decrypted data size written to `out`, which is +/// often shorter than `session_attachment_decrypted_max_size(datalen)` (because of removed +/// padding). Not touched if the function returns false. +/// - `error` -- if non-NULL and decryption fails then a reason for the error is written here; must +/// be a buffer of at least 256 bytes (or NULL). +/// +/// Outputs: +/// - returns true if decryption succeeds: the `*outlen` bytes decrypted value will be written +/// starting at `out`. Returns false decryption fails (in which case `error` will be written with +/// the error reason, if provided). +LIBSESSION_EXPORT bool session_attachment_decrypt( + const unsigned char* data, + size_t datalen, + const unsigned char* key, + unsigned char* out, + size_t* outlen, + char* error); + +/// API: crypto/session_attachment_decrypt +/// +/// Decrypts an attachment allegedly produced by session_attachment_encrypt into a single in-memory +/// allocated buffer. +/// +/// Inputs: +/// - `data` -- pointer to the buffer of data to decrypt +/// - `datalen` -- length of the `data` +/// - `key` -- pointer to the 32-byte binary decryption key +/// - `out` -- Pointer-pointer to an output buffer; a new buffer is allocated, the decrypted +/// attachment written to it, and then the pointer to that buffer is stored here. This buffer +/// must be `free()`d by the caller when done with it *unless* the function returns false, in +/// which case the buffer pointer will not be set. +/// - `outlen` -- pointer in which to store final decrypted data size. Not touched if the function +/// returns false. +/// - `error` -- if non-NULL and decryption fails then a reason for the error is written here; must +/// be a buffer of at least 256 bytes (or NULL). +/// +/// Outputs: +/// - returns true if decryption succeeds and `out` was set to the decrypted data; false if +/// decryption fails (in which case `error` will be written with the error reason, if provided). +LIBSESSION_EXPORT bool session_attachment_decrypt_alloc( + const unsigned char* data, + size_t datalen, + const unsigned char* key, + unsigned char** out, + size_t* outlen, + char* error); + +/// API: crypto/session_attachment_decrypt_file +/// +/// Decrypts an attachment from a file on disk and loads the decrypted content into an in-memory +/// buffer. +/// +/// Inputs: +/// - `file_in` -- C string of input filename +/// - `datalen` -- length of the `data` +/// - `key` -- pointer to the 32-byte binary decryption key +/// - `make_buffer` -- callback to invoke to allocate a buffer of the required size into which to +/// write the decrypted data. It is passed two arguments: the required output buffer size, and +/// `ctx`, and must return a pointer to the beginning of a buffer of the requested size in which +/// to write the encrypted data. Can return NULL to abort encryption (e.g. if the size is too +/// large). Note that some of the buffer may not end up being used, due to padding: the return +/// value of `session_attachment_decrypt_file` indicates the actual amount of decrypted data +/// written to the buffer. +/// - `ctx` - arbitrary pointer (which can be null) to pass to make_buffer to pass user-defined +/// context into the callback. This encrypt function does not otherwise touch the pointer. +/// - `error` -- if non-NULL and decryption fails then a reason for the error is written here; must +/// be a buffer of at least 256 bytes (or NULL). +/// +/// Outputs: +/// - returns the amount of decrypted data written to the buffer, (which can be 0, for an empty +/// encrypted file!). If decryption fails this will be set to `(size_t)-1` to indicate the +/// failure, and `error` will be written with the error reason, if provided. +/// +LIBSESSION_EXPORT size_t session_attachment_decrypt_file( + const char* file_in, + const unsigned char* key, + unsigned char* (*make_buffer)(size_t, void* ctx), + void* ctx, + char* error); + +/// API: crypto/session_attachment_decrypt_to_file +/// +/// Decrypts an attachment from a in-memory buffer writing the decrypted data to a file on disk. +/// +/// Inputs: +/// - `data` -- pointer to the buffer of data to decrypt +/// - `datalen` -- length of the `data` +/// - `key` -- pointer to the 32-byte binary decryption key +/// - `filename` -- C string of output filename to write the decrypted data to. The file will be +/// overwritten if it exists. If a failure occurs during decryption the file will be removed. +/// - `error` -- if non-NULL and decryption fails then a reason for the error is written here; must +/// be a buffer of at least 256 bytes (or NULL). +/// +/// Outputs: +/// - returns true if decryption succeeds; false if decryption fails (in which case `error` will be +/// written with the error reason, if provided). +/// +LIBSESSION_EXPORT bool session_attachment_decrypt_to_file( + const unsigned char* data, + size_t datalen, + const unsigned char* key, + const char* filename, + char* error); + +/// API: crypto/session_attachment_decrypt_file_to_file +/// +/// Decrypts an attachment from an encrypted file on disk to another path on disk. +/// +/// - `file_in` -- C string input filename containing encrypted data. +/// - `key` -- pointer to the 32-byte binary decryption key +/// - `file_out` -- C string of output filename to write the decrypted data to. The file will be +/// overwritten if it exists. If a failure occurs during decryption the file will be removed. +/// - `error` -- if non-NULL and decryption fails then a reason for the error is written here; must +/// be a buffer of at least 256 bytes (or NULL). +/// +/// Outputs: +/// - returns true if decryption succeeds; false if decryption fails (in which case `error` will be +/// written with the error reason, if provided). +LIBSESSION_EXPORT bool session_attachment_decrypt_file_to_file( + const char* file_in, const unsigned char* key, const char* file_out, char* error); + +#ifdef __cplusplus +} +#endif diff --git a/include/session/attachments.hpp b/include/session/attachments.hpp index a63c53a4..3cb568f1 100644 --- a/include/session/attachments.hpp +++ b/include/session/attachments.hpp @@ -36,12 +36,12 @@ constexpr size_t ENCRYPT_KEY_SIZE = 32; // retrieved via oxen-storage-server onion requests, and its padded size is the maximum attachment // size allowed by the storage server. (Technically this value was chosen as it is the largest // unencrypted data size that has the same padded+encrypted size as a 10'000'000B file). -constexpr size_t ENCRYPT_MAX_SIZE = +constexpr size_t MAX_REGULAR_SIZE = 10218286; // == 10223616 after stream mac+tag and (1-byte) padding // Returns the amount of padding to add to an attachment to obfuscate the true size, given -// crypto_secretstream encryption with a 32kiB chunk size. We determine the padded size as follows, -// given an input size N: +// crypto_secretstream encryption with a 32kiB chunk size. We determine the padded size as +// follows, given an input size N: // // - compute the total raw size M as N plus: // - 1 for the 'S' prefix (outside the encryption) @@ -65,11 +65,24 @@ constexpr size_t ENCRYPT_MAX_SIZE = // + 31 × 17 -- embedded mac+tags after every 32kiB of file stream data // = 1015808 final output. // -// (Note that we always including at least one padding byte, and there are some complications in the -// calculation as padding values get large enough to start inducing additional mac+tags; see the -// implementation for details). +// (Note that we always including at least one padding byte, and there are some complications in +// the calculation as padding values get large enough to start inducing additional mac+tags; see +// the implementation for details). size_t encrypted_padding(size_t data_size); +/// API: crypto/attachment::encrypted_size +/// +/// Returns the exact final encrypted (including any overhead and padding) of an input of +/// `plaintext_size`. +size_t encrypted_size(size_t plaintext_size); + +/// API: crypto/attachment::decrypted_max_size +/// +/// Returns the maximum possible decrypted size of encrypted data of length `encrypted_size`. The +/// actual size can be (and usually is) less than this depending on how much padding is in the data. +/// Returns std::nullopt if the input is too small to be a valid encrypted attachment. +std::optional decrypted_max_size(size_t encrypted_size); + /// API: crypto/attachment::encrypt /// /// Encrypt an attachment for storage on the file server and distribution to other users using @@ -118,9 +131,105 @@ std::pair, std::array> encry Domain domain, bool allow_large = false); +/// API: crypto/attachment::encrypt +/// +/// Similar to the above `encrypt` except that instead of allocating and returning a vector it +/// writes the encrypted result directly into a given output span. The output span *must* be +/// exactly `encrypted_size()` bytes long (but this is checked via assertion in debug builds). +/// +/// Inputs: +/// - `seed` -- as above +/// - `data` -- as above +/// - `domain` -- as above +/// - `out` -- writeable span into which the encrypted data will be written. This span must be +/// exactly `encrypted_size(data.size())` bytes long. +/// - `allow_large` -- as above. +/// +/// Outputs: +/// - 32 byte decryption key +/// +/// Throws std::invalid_argument if `seed` is shorter than 32 bytes, or if data is larger than +/// MAX_REGULAR_SIZE (unless `allow_large` is true). +std::array encrypt( + std::span seed, + std::span data, + Domain domain, + std::span out, + bool allow_large = false); + +/// API: crypto/attachment::encrypt +/// +/// Encrypts the contents of a file on disk into a buffer. This requires reading the file twice +/// (once in order to generate the deterministic encryption key and nonce, and then a second time +/// for the actual encryption), but does not require holding the file contents in memory. +/// +/// Inputs: +/// - `seed`, `domain`, `allow_large` -- see above. +/// - `file` -- path to the file to encrypt. +/// +/// Outputs: +/// - Pair of values: the padded+encrypted data, and the decryption key (32 bytes), both in raw +/// bytes. +/// +/// Throws std::invalid_argument if `seed` is shorter than 32 bytes, or if the file is larger than +/// MAX_REGULAR_SIZE. +std::pair, std::array> encrypt( + std::span seed, + const std::filesystem::path& file, + Domain domain, + bool allow_large = false); + +/// API: crypto/attachment::encrypt +/// +/// Encrypts the contents of a file on disk into a buffer. This method is a more general version of +/// the above that allows allocation of the encrypted buffer via a callback once the size is +/// determined from the file. +/// +/// Inputs: +/// - `seed`, `domain`, `allow_large` -- see above. +/// - `file` -- path to the file to encrypt. +/// - `make_buffer` -- callback that is invoked with the exact required encrypted size for the file +/// that must return a byte span of that exact file where the encrypted data will be written. +/// +/// Outputs: +/// - The 32-byte decryption key, in raw bytes. +/// +/// Throws std::invalid_argument if `seed` is shorter than 32 bytes, or if the file is larger than +/// MAX_REGULAR_SIZE. +/// Throws std::runtime_error if the file size changes between first and second passes. +std::array encrypt( + std::span seed, + const std::filesystem::path& file, + Domain domain, + std::function(size_t enc_size)> make_buffer, + bool allow_large = false); + +/// API: crypto/attachment::encrypt +/// +/// Encrypts the contents of a plaintext buffer, writing the encrypted data to a file. The file +/// will be overwritten. +/// +/// Inputs: +/// - `seed`, `domain`, `allow_large` -- see above. +/// - `data` -- the buffer of data to encrypt. +/// - `file` -- path to the file to write to. +/// +/// Outputs: +/// - The 32-byte decryption key, in raw bytes. +/// +/// Throws std::invalid_argument if `seed` is shorter than 32 bytes, or if data is larger than +/// MAX_REGULAR_SIZE (unless `allow_large` is given). Throws on I/O error. If decryption fails +/// then any partially written output file will be removed. +std::array encrypt( + std::span seed, + std::span data, + Domain domain, + const std::filesystem::path& file, + bool allow_large = false); + /// API: crypto/attachment::decrypt /// -/// Decrypts an attachment allegedly produced by attachment::encrypt to a single in-memory buffer. +/// Decrypts an attachment allegedly produced by attachment::encrypt to an in-memory byte vector. /// /// Inputs: /// - `data` -- in-memory buffer of data to decrypt. @@ -133,6 +242,28 @@ std::pair, std::array> encry std::vector decrypt( std::span encrypted, std::span key); +/// API: crypto/attachment::decrypt +/// +/// Decrypts an attachment allegedly produced by attachment::encrypt to a single in-memory, +/// caller-provided buffer. This version writes into a given output span rather than allocating a +/// new vector. +/// +/// Inputs: +/// - `data` -- in-memory buffer of data to decrypt. +/// - `key` -- the 32-byte decryption key +/// - `out` -- writeable output span in which the decrypted value should be written. The given span +/// must be at least `decrypted_max_size(data.size())` bytes large. +/// +/// Outputs: +/// - size_t -- the actual decrypted data size written into `out` which could be (and often is, due +/// to padding) less than `out.size()`. +/// +/// Throws std::runtime_error if decryption fails. +size_t decrypt( + std::span encrypted, + std::span key, + std::span out); + /// API: crypto/attachment::Decryptor /// /// Object-based interfaced to streaming decryption. The basic usage is to construct the object @@ -205,4 +336,67 @@ void decrypt( std::span key, const std::filesystem::path& filename); +/// API: crypto/attachment::decrypt +/// +/// Decrypts an encrypted attachment stored in an input file into a byte vector. +/// +/// Inputs: +/// - `filename` -- path to encrypted file. +/// - `key` -- the 32-byte decryption key. +/// +/// Outputs: +/// - vector of decrypted content. +/// +/// Throws std::runtime_error if decryption fails; can throw I/O exceptions if reading the file +/// fails. +std::vector decrypt( + const std::filesystem::path& encrypted_file, + std::span key); + +/// API: crypto/attachment::decrypt +/// +/// Decrypts an encrypted attachment stored in an input file into a provided memory buffer. +/// +/// Inputs: +/// - `filename` -- path to encrypted file. +/// - `key` -- the 32-byte decryption key. +/// - `make_buffer` -- callback that is invoked to allocate the buffer into which the content should +/// be written. This is passed the required buffer size. Note that this buffer may not be +/// completely filled: the return value of `decrypt()` indicates the actual amount of the buffer +/// that was written. +/// +/// Outputs: +/// - size_t the actual decrypted size. Can be less than the value passed to `decrypted.size()` +/// because of padding. +/// +/// Throws std::runtime_error if decryption fails; can throw I/O exceptions if reading the file +/// fails. +size_t decrypt( + const std::filesystem::path& encrypted_file, + std::span key, + std::function(size_t dec_size)> make_buffer); + +/// API: crypto/attachment::decrypt +/// +/// Decrypts an attachment allegedly produced by attachment::encrypt stored in a file to another +/// output file. Overwrites the destination file if it already exists. +/// +/// Unlike the various decrypt functions above, this version does not need to hold more than a few +/// kB of the input/output file in memory at a time, regardless of the size of the input or output +/// files. +/// +/// Inputs: +/// - `file_in` -- filename containing the data to decrypt. +/// - `key` -- the 32-byte decryption key. +/// - `file_out` -- where to write the output file. +/// +/// Outputs: None. +/// +/// Throws std::runtime_error if decryption fails or if writing to the file fails. Upon exception a +/// partially written file will be deleted. +void decrypt( + const std::filesystem::path& file_in, + std::span key, + const std::filesystem::path& file_out); + } // namespace session::attachment diff --git a/src/attachments.cpp b/src/attachments.cpp index 269a3968..6efb8c89 100644 --- a/src/attachments.cpp +++ b/src/attachments.cpp @@ -1,16 +1,23 @@ +#include "session/attachments.hpp" + +#include #include #include #include +#include #include #include #include +#include +#include #include -#include #include #include #include +#include "internal-util.hpp" + namespace session::attachment { using namespace oxen::log::literals; @@ -59,6 +66,31 @@ size_t encrypted_padding(size_t data_size) { return padding; } +size_t encrypted_size(size_t plaintext_size) { + size_t padding = encrypted_padding(plaintext_size); + size_t padded_size = plaintext_size + padding; + size_t tags_size = + (padded_size + ENCRYPT_CHUNK_SIZE - 1) / ENCRYPT_CHUNK_SIZE * ENCRYPT_CHUNK_OVERHEAD; + + return 1 /* 'S' identifier */ + ENCRYPT_HEADER + plaintext_size + padding + tags_size; +} + +std::optional decrypted_max_size(size_t encrypted_size) { + size_t sz = encrypted_size - 1 /* 'S' identifier */ - 1 /* minimum padding length */ - + ENCRYPT_HEADER; + + // The data is chunked into 32kiB+17B chunks (32kiB of data + 17 bytes of per-chunk stream + // tag+mac), so we can figure out how many 17 byte overhead values should be present: + size_t overhead = + (sz + ENCRYPTED_CHUNK_TOTAL - 1) / ENCRYPTED_CHUNK_TOTAL * ENCRYPT_CHUNK_OVERHEAD; + + sz -= overhead; + + if (sz > encrypted_size) // Overflow + return std::nullopt; + return sz; +} + // We have to roll our own custom version of crypto_secretstream_xchacha20poly1305_init_push here // because libsodium offers no way to provide the randomness it uses (it hard codes a call to // randombytes_buf), and so this repeats its internal implementation but using our hashed data for @@ -86,43 +118,22 @@ secretstream_xchacha20poly1305_init_push_with_nonce( return st; } -std::pair, std::array> encrypt( - std::span seed, - std::span data, - Domain domain, - bool allow_large) { - - if (seed.size() < 32) - throw std::invalid_argument{"attachment::encrypt requires a 32-byte uploader seed"}; - - if (data.size() > ENCRYPT_MAX_SIZE && !allow_large) - throw std::invalid_argument{"data to encrypt is too large"}; - - std::pair, std::array> result; - auto& [out, key] = result; - - std::span udata{ - reinterpret_cast(data.data()), data.size()}; - - std::array nonce_key; - - crypto_generichash_blake2b_state b_st; - const auto domain_byte = static_cast(domain); - crypto_generichash_blake2b_init(&b_st, &domain_byte, 1, nonce_key.size()); - crypto_generichash_blake2b_update( - &b_st, reinterpret_cast(seed.data()), 32); - crypto_generichash_blake2b_update(&b_st, udata.data(), udata.size()); - crypto_generichash_blake2b_final(&b_st, nonce_key.data(), nonce_key.size()); - std::memcpy(key.data(), nonce_key.data() + ENCRYPT_HEADER, ENCRYPT_KEY_SIZE); - - size_t padding = encrypted_padding(data.size()); +// Encryption implementation function. `get_chunk(N)` returns a pair of [span, bool] of the next N (max ENCRYPT_CHUNK_SIZE) bytes, less than N only at the end of the +// input, where the bool is true if there is at least 1 byte more of data to be retrieved (i.e. +// false means the end of the data). It may not return an empty chunk except for the very first +// call. +template ReadData> +static void encrypt_impl( + std::span out, + size_t data_size, + std::span nonce_key, + ReadData get_chunk) { + size_t padding = encrypted_padding(data_size); assert(padding >= 1); + size_t padded_size = data_size + padding; - size_t padded_size = data.size() + padding; - size_t tags_size = - (padded_size + ENCRYPT_CHUNK_SIZE - 1) / ENCRYPT_CHUNK_SIZE * ENCRYPT_CHUNK_OVERHEAD; - - out.resize(1 + ENCRYPT_HEADER + data.size() + padding + tags_size); + assert(out.size() == encrypted_size(data_size)); out[0] = std::byte{'S'}; std::span uout{reinterpret_cast(out.data()), out.size()}; @@ -130,19 +141,19 @@ std::pair, std::array> encry std::span header{uout.data() + 1, ENCRYPT_HEADER}; auto st = secretstream_xchacha20poly1305_init_push_with_nonce( - header, as_span(std::span{key}), std::span{nonce_key}.first()); + header, nonce_key.last(), nonce_key.first()); auto* outpos = uout.data() + 1 + ENCRYPT_HEADER; auto* const outend = uout.data() + uout.size(); - auto* inpos = udata.data(); - auto* const inend = inpos + udata.size(); // Now we build a buffer containing padding, plus whatever initial actual data goes on the end // of the last chunk of padding: + bool done = false; { std::vector buf; buf.reserve(std::min(ENCRYPT_CHUNK_SIZE, padded_size)); for (size_t padding_remaining = padding; padding_remaining;) { + unsigned char tag = 0; if (padding_remaining > ENCRYPT_CHUNK_SIZE) { // Full chunk of 0x00 padding (with more padding in the next chunk) buf.resize(ENCRYPT_CHUNK_SIZE); @@ -150,18 +161,19 @@ std::pair, std::array> encry } else { buf.resize(padding_remaining - 1); // 0x00 padding buf.push_back(0x01); // padding terminator - if (size_t first_data = - std::min(ENCRYPT_CHUNK_SIZE - padding_remaining, udata.size())) { - buf.insert(buf.end(), inpos, inpos + first_data); - inpos += first_data; - } + auto [chunk, more] = get_chunk(ENCRYPT_CHUNK_SIZE - padding_remaining); + assert(chunk.size() == ENCRYPT_CHUNK_SIZE - padding_remaining || !more); + if (!chunk.empty()) + buf.insert(buf.end(), chunk.begin(), chunk.end()); padding_remaining = 0; + if (!more) { + tag = crypto_secretstream_xchacha20poly1305_TAG_FINAL; + done = true; + } } assert(outpos + buf.size() + crypto_secretstream_xchacha20poly1305_ABYTES <= outend); - unsigned char tag = inpos < inend ? 0 : crypto_secretstream_xchacha20poly1305_TAG_FINAL; - unsigned long long out_len; crypto_secretstream_xchacha20poly1305_push( &st, outpos, &out_len, buf.data(), buf.size(), nullptr, 0, tag); @@ -172,28 +184,289 @@ std::pair, std::array> encry // Now we're through the initial padding (and probably some initial data): now all we need to do // is push the rest of the data - while (inpos < inend) { - auto* chunk_start = inpos; - inpos = std::min(chunk_start + ENCRYPT_CHUNK_SIZE, inend); - assert(outpos + (inpos - chunk_start) + crypto_secretstream_xchacha20poly1305_ABYTES <= - outend); - unsigned char tag = inpos < inend ? 0 : crypto_secretstream_xchacha20poly1305_TAG_FINAL; + while (!done) { + auto [chunk, more] = get_chunk(ENCRYPT_CHUNK_SIZE); + assert(!chunk.empty()); + assert(chunk.size() == ENCRYPT_CHUNK_SIZE || !more); + assert(outpos + chunk.size() + crypto_secretstream_xchacha20poly1305_ABYTES <= outend); + + unsigned char tag = more ? 0 : crypto_secretstream_xchacha20poly1305_TAG_FINAL; unsigned long long out_len; crypto_secretstream_xchacha20poly1305_push( - &st, outpos, &out_len, chunk_start, inpos - chunk_start, nullptr, 0, tag); - assert(out_len == inpos - chunk_start + crypto_secretstream_xchacha20poly1305_ABYTES); + &st, outpos, &out_len, chunk.data(), chunk.size(), nullptr, 0, tag); + assert(out_len == chunk.size() + crypto_secretstream_xchacha20poly1305_ABYTES); outpos += out_len; + if (!more) + done = true; } +} + +static std::tuple< + std::array, + std::array, + const unsigned char*, + const unsigned char*> +encrypt_buffer_init( + std::span seed, + std::span data, + Domain domain, + bool allow_large) { + std::tuple< + std::array, + std::array, + const unsigned char*, + const unsigned char*> + result; + auto& [nonce_key, key, inpos, inend] = result; + + if (seed.size() < 32) + throw std::invalid_argument{"attachment::encrypt requires a 32-byte uploader seed"}; + + if (data.size() > MAX_REGULAR_SIZE && !allow_large) + throw std::invalid_argument{"data to encrypt is too large"}; + + std::span udata{ + reinterpret_cast(data.data()), data.size()}; + + crypto_generichash_blake2b_state b_st; + const auto domain_byte = static_cast(domain); + crypto_generichash_blake2b_init(&b_st, &domain_byte, 1, nonce_key.size()); + crypto_generichash_blake2b_update( + &b_st, reinterpret_cast(seed.data()), 32); + crypto_generichash_blake2b_update(&b_st, udata.data(), udata.size()); + crypto_generichash_blake2b_final(&b_st, nonce_key.data(), nonce_key.size()); + std::memcpy(key.data(), nonce_key.data() + ENCRYPT_HEADER, ENCRYPT_KEY_SIZE); + + inpos = udata.data(); + inend = inpos + udata.size(); return result; } -std::vector decrypt( - std::span encrypted, std::span key) { +std::array encrypt( + std::span seed, + std::span data, + Domain domain, + std::span out, + bool allow_large) { + + auto [nonce_key, key, inpos, inend] = encrypt_buffer_init(seed, data, domain, allow_large); + + encrypt_impl( + out, + data.size(), + nonce_key, + [&inpos, &inend](size_t size) -> std::pair, bool> { + auto* start = inpos; + auto* end = std::min(inpos + size, inend); + inpos = end; + return {{start, end}, inpos != inend}; + }); + + return key; +} + +std::pair, std::array> encrypt( + std::span seed, + std::span data, + Domain domain, + bool allow_large) { + + if (seed.size() < 32) + throw std::invalid_argument{"attachment::encrypt requires a 32-byte uploader seed"}; + + if (data.size() > MAX_REGULAR_SIZE && !allow_large) + throw std::invalid_argument{"data to encrypt is too large"}; + + std::pair, std::array> result; + auto& [out, key] = result; + + out.resize(encrypted_size(data.size())); + + key = encrypt(seed, data, domain, out, allow_large); + + return result; +} + +std::array encrypt( + std::span seed, + const std::filesystem::path& file, + Domain domain, + std::function(size_t enc_size)> make_buffer, + bool allow_large) { + + std::ifstream in; + in.exceptions(std::ios::failbit | std::ios::badbit); + in.open(file, std::ios::binary | std::ios::ate); + size_t size = in.tellg(); + in.seekg(0, std::ios::beg); + + size = encrypted_size(size); + + std::array nonce_key; + + crypto_generichash_blake2b_state b_st; + const auto domain_byte = static_cast(domain); + crypto_generichash_blake2b_init(&b_st, &domain_byte, 1, nonce_key.size()); + crypto_generichash_blake2b_update( + &b_st, reinterpret_cast(seed.data()), 32); + + size_t in_size = 0; + std::array chunk; + while (auto sz = in.readsome(reinterpret_cast(chunk.data()), chunk.size())) { + crypto_generichash_blake2b_update( + &b_st, reinterpret_cast(chunk.data()), sz); + in_size += sz; + } + crypto_generichash_blake2b_final(&b_st, nonce_key.data(), nonce_key.size()); + + std::array key; + std::memcpy(key.data(), nonce_key.data() + ENCRYPT_HEADER, ENCRYPT_KEY_SIZE); + + in.seekg(0, std::ios::beg); + in.clear(); + + auto encrypted = make_buffer(size); + if (encrypted.size() != size) + throw std::logic_error{ + "make_buffer returned span of invalid size: expected {}, got {}"_format( + size, encrypted.size())}; + + std::array buf; + encrypt_impl( + encrypted, + in_size, + nonce_key, + [&in, &in_size, &buf](size_t size) -> std::pair, bool> { + size_t consumed = in.tellg(); + if (consumed + size > in_size) + size = in_size - consumed; + + if (size > 0) + in.read(reinterpret_cast(buf.data()), size); + + in.peek(); + return {std::span{buf}.first(size), !in.eof()}; + }); + + return key; +} + +std::pair, std::array> encrypt( + std::span seed, + const std::filesystem::path& file, + Domain domain, + bool allow_large) { + + std::pair, std::array> result; + auto& [encrypted, key] = result; + + key = encrypt( + seed, + file, + domain, + [&encrypted](size_t enc_size) { + encrypted.resize(enc_size); + return std::span{encrypted}; + }, + allow_large); - if (encrypted.size() <= 1 + ENCRYPT_HEADER + ENCRYPT_CHUNK_OVERHEAD) + return result; +} + +std::array encrypt( + std::span seed, + std::span data, + Domain domain, + const std::filesystem::path& file, + bool allow_large) { + + auto [nonce_key, key, inpos, inend] = encrypt_buffer_init(seed, data, domain, allow_large); + + size_t padding = encrypted_padding(data.size()); + assert(padding >= 1); + size_t padded_size = data.size() + padding; + + try { + std::ofstream out; + out.exceptions(std::ios::failbit | std::ios::badbit); + out.open(file, std::ios::binary | std::ios::trunc); + out.write("S", 1); + + std::array cbuf; + std::span ubuf{reinterpret_cast(cbuf.data()), cbuf.size()}; + + auto st = secretstream_xchacha20poly1305_init_push_with_nonce( + ubuf.first(), + std::span{nonce_key}.last(), + std::span{nonce_key}.first()); + + out.write(cbuf.data(), ENCRYPT_HEADER); + + // Now we build a buffer containing padding, plus whatever initial actual data goes on the + // end of the last chunk of padding, and write those encrypted padding chunks to the file: + { + std::vector buf; + buf.reserve(std::min(ENCRYPT_CHUNK_SIZE, padded_size)); + for (size_t padding_remaining = padding; padding_remaining;) { + if (padding_remaining > ENCRYPT_CHUNK_SIZE) { + // Full chunk of 0x00 padding (with more padding in the next chunk) + buf.resize(ENCRYPT_CHUNK_SIZE); + padding_remaining -= ENCRYPT_CHUNK_SIZE; + } else { + buf.resize(padding_remaining - 1); // 0x00 padding + buf.push_back(0x01); // padding terminator + if (size_t first_data = + std::min(ENCRYPT_CHUNK_SIZE - padding_remaining, data.size())) { + buf.insert(buf.end(), inpos, inpos + first_data); + inpos += first_data; + } + padding_remaining = 0; + } + + unsigned char tag = + inpos < inend ? 0 : crypto_secretstream_xchacha20poly1305_TAG_FINAL; + + unsigned long long out_len; + crypto_secretstream_xchacha20poly1305_push( + &st, ubuf.data(), &out_len, buf.data(), buf.size(), nullptr, 0, tag); + assert(out_len == buf.size() + crypto_secretstream_xchacha20poly1305_ABYTES); + out.write(cbuf.data(), out_len); + } + } + + // Now we're through the initial padding (and probably some initial data): now all we need + // to do is write the rest of the data in chunks + while (inpos < inend) { + auto* chunk_start = inpos; + inpos = std::min(chunk_start + ENCRYPT_CHUNK_SIZE, inend); + unsigned char tag = inpos < inend ? 0 : crypto_secretstream_xchacha20poly1305_TAG_FINAL; + + unsigned long long out_len; + crypto_secretstream_xchacha20poly1305_push( + &st, ubuf.data(), &out_len, chunk_start, inpos - chunk_start, nullptr, 0, tag); + assert(out_len == inpos - chunk_start + crypto_secretstream_xchacha20poly1305_ABYTES); + + out.write(cbuf.data(), out_len); + } + } catch (const std::exception& e) { + std::error_code ec; + std::filesystem::remove(file, ec); + throw; + } + + return key; +} + +size_t decrypt( + std::span encrypted, + std::span key, + std::span out) { + + auto max_size = decrypted_max_size(encrypted.size()); + if (!max_size) throw std::runtime_error{"Attachment decryption failed: encrypted data too short"}; if (encrypted.front() != std::byte{'S'}) @@ -201,11 +474,13 @@ std::vector decrypt( "Attachment decryption failed: unknown encryption type 0x{:02x}; expected 0x53 (S)"_format( +static_cast(encrypted.front()))}; + if (out.size() < *max_size) + throw std::logic_error{ + "Attachment decryption failed: output buffer too small to decrypt contents"}; + std::span uenc{ reinterpret_cast(encrypted.data()), encrypted.size()}; - auto header = uenc.subspan<1, ENCRYPT_HEADER>(); - crypto_secretstream_xchacha20poly1305_state st; crypto_secretstream_xchacha20poly1305_init_pull( &st, uenc.data() + 1, reinterpret_cast(key.data())); @@ -213,7 +488,7 @@ std::vector decrypt( auto* inpos = uenc.data() + 1 + ENCRYPT_HEADER; auto* const inend = uenc.data() + uenc.size(); - std::vector decrypted; + std::byte* decrypted = out.data(); bool done = false; // Discard any leading padding chunks (of which there is *always* at least 1 because we always @@ -263,8 +538,8 @@ std::vector decrypt( size_t final_size = init_data.size() + (inend - inpos) - (inend - inpos + ENCRYPTED_CHUNK_TOTAL - 1) / ENCRYPTED_CHUNK_TOTAL * ENCRYPT_CHUNK_OVERHEAD; - decrypted.reserve(final_size); - decrypted.insert(decrypted.end(), padend, padbuf.end()); + assert(out.size() >= final_size); + decrypted = std::copy(padend, padbuf.end(), decrypted); if (tag == crypto_secretstream_xchacha20poly1305_TAG_FINAL) { if (inpos != inend) @@ -289,15 +564,12 @@ std::vector decrypt( "Attachment decryption failed: data ended before end of stream"}; size_t chunk_size = std::min(inend - inpos - ENCRYPT_CHUNK_OVERHEAD, ENCRYPT_CHUNK_SIZE); - assert(decrypted.capacity() >= decrypted.size() + chunk_size); - decrypted.resize(decrypted.size() + chunk_size); - auto* out = - reinterpret_cast(decrypted.data() + decrypted.size() - chunk_size); + assert(decrypted + chunk_size <= out.data() + out.size()); unsigned char tag; if (crypto_secretstream_xchacha20poly1305_pull( &st, - out, + reinterpret_cast(decrypted), nullptr, &tag, inpos, @@ -306,6 +578,7 @@ std::vector decrypt( 0) != 0) throw std::runtime_error{"Attachment decryption failed: invalid key or corrupted data"}; + decrypted += chunk_size; inpos += chunk_size + ENCRYPT_CHUNK_OVERHEAD; if (tag == crypto_secretstream_xchacha20poly1305_TAG_FINAL) { @@ -319,7 +592,28 @@ std::vector decrypt( } } - return decrypted; + return decrypted - out.data(); +} + +std::vector decrypt( + std::span encrypted, std::span key) { + + auto max_size = decrypted_max_size(encrypted.size()); + if (!max_size) + throw std::runtime_error{"Attachment decryption failed: encrypted data too short"}; + + if (encrypted.front() != std::byte{'S'}) + throw std::runtime_error{ + "Attachment decryption failed: unknown encryption type 0x{:02x}; expected 0x53 (S)"_format( + +static_cast(encrypted.front()))}; + + std::vector result; + result.resize(*max_size); + + size_t actual = decrypt(encrypted, key, result); + result.resize(actual); + + return result; } Decryptor::Decryptor( @@ -499,4 +793,278 @@ void decrypt( } } +size_t decrypt( + const std::filesystem::path& encrypted_file, + std::span key, + std::function(size_t dec_size)> make_buffer) { + + std::ifstream in; + in.exceptions(std::ios::failbit | std::ios::badbit); + in.open(encrypted_file, std::ios::binary | std::ios::ate); + size_t size = in.tellg(); + in.seekg(0, std::ios::beg); + + auto max_size = decrypted_max_size(size); + if (!max_size) + throw std::runtime_error{ + "Decryption failed: file is too small to contain an encrypted attachment"}; + auto out = make_buffer(*max_size); + if (out.size() != *max_size) + throw std::logic_error{ + "make_buffer returned span of invalid size: expected {}, got {}"_format( + *max_size, out.size())}; + + auto decrypted = out.begin(); + auto end = out.end(); + Decryptor d{key, [&decrypted, &end](std::span data) { + if (data.size() > end - decrypted) + throw std::runtime_error{ + "Decryption failed: output span is too small to contain decrypted " + "data"}; + decrypted = std::copy(data.begin(), data.end(), decrypted); + }}; + + std::array chunk; + while (auto sz = in.readsome(reinterpret_cast(chunk.data()), chunk.size())) + d.update(std::span{chunk}.first(sz)); + d.finalize(); + + return decrypted - out.begin(); +} + +std::vector decrypt( + const std::filesystem::path& encrypted_file, + std::span key) { + + std::vector plaintext; + size_t actual = decrypt(encrypted_file, key, [&plaintext](size_t size) { + plaintext.resize(size); + return std::span{plaintext}; + }); + plaintext.resize(actual); + return plaintext; +} + +void decrypt( + const std::filesystem::path& file_in, + std::span key, + const std::filesystem::path& file_out) { + + try { + std::ifstream in; + in.exceptions(std::ios::failbit | std::ios::badbit); + in.open(file_in, std::ios::binary | std::ios::ate); + size_t size = in.tellg(); + in.seekg(0, std::ios::beg); + + auto max_size = decrypted_max_size(size); + if (!max_size) + throw std::runtime_error{ + "Decryption failed: file is too small to contain an encrypted attachment"}; + + std::ofstream out; + out.exceptions(std::ios::failbit | std::ios::badbit); + out.open(file_out, std::ios::binary | std::ios::trunc); + + Decryptor d{key, [&out](std::span data) { + out.write(reinterpret_cast(data.data()), data.size()); + }}; + + std::array chunk; + while (auto sz = in.readsome(reinterpret_cast(chunk.data()), chunk.size())) + d.update(std::span{chunk}.first(sz)); + d.finalize(); + } catch (const std::exception& e) { + std::error_code ec; + std::filesystem::remove(file_out, ec); + throw; + } +} + } // namespace session::attachment + +extern "C" { + +using namespace session; + +const size_t ATTACHMENT_ENCRYPT_KEY_SIZE = attachment::ENCRYPT_KEY_SIZE; +const size_t ATTACHMENT_MAX_REGULAR_SIZE = attachment::MAX_REGULAR_SIZE; + +LIBSESSION_C_API size_t session_attachment_encrypted_size(size_t plaintext_len) { + return attachment::encrypted_size(plaintext_len); +} + +LIBSESSION_C_API size_t session_attachment_decrypted_max_size(size_t encrypted_len) { + return attachment::decrypted_max_size(encrypted_len) + .value_or(std::numeric_limits::max()); +} + +LIBSESSION_C_API bool session_attachment_encrypt( + const unsigned char* seed, + const unsigned char* data, + size_t datalen, + ATTACHMENT_DOMAIN domain, + unsigned char* key_out, + unsigned char* out, + char* error) { + try { + auto key = attachment::encrypt( + std::span{reinterpret_cast(seed), 32}, + std::span{reinterpret_cast(data), datalen}, + static_cast(domain), + std::span{reinterpret_cast(out), attachment::encrypted_size(datalen)}, + /*allow_large=*/true); + std::memcpy(key_out, key.data(), key.size()); + sodium_zero_buffer(key.data(), key.size()); + return true; + } catch (const std::exception& e) { + return set_error(error, e); + } +} + +LIBSESSION_C_API bool session_attachment_decrypt( + const unsigned char* data, + size_t datalen, + const unsigned char* key, + unsigned char* out, + size_t* outlen, + char* error) { + + try { + auto max_size = attachment::decrypted_max_size(datalen); + if (!max_size) + throw std::runtime_error{"encrypted data too small"}; + + *outlen = attachment::decrypt( + std::span{reinterpret_cast(data), datalen}, + std::span{ + reinterpret_cast(key), attachment::ENCRYPT_KEY_SIZE}, + std::span{reinterpret_cast(out), *max_size}); + return true; + } catch (const std::exception& e) { + return set_error(error, e); + } +} + +LIBSESSION_C_API bool session_attachment_decrypt_alloc( + const unsigned char* data, + size_t datalen, + const unsigned char* key, + unsigned char** out, + size_t* outlen, + char* error) { + std::byte* decrypted = nullptr; + try { + auto max_size = attachment::decrypted_max_size(datalen); + if (!max_size) + throw std::runtime_error{"encrypted data too small"}; + + auto* decrypted = static_cast(std::malloc(*max_size)); + *outlen = attachment::decrypt( + std::span{reinterpret_cast(data), datalen}, + std::span{ + reinterpret_cast(key), attachment::ENCRYPT_KEY_SIZE}, + std::span{decrypted, *max_size}); + *out = reinterpret_cast(decrypted); + return true; + } catch (const std::exception& e) { + if (decrypted) + std::free(decrypted); + return set_error(error, e); + } +} + +LIBSESSION_C_API size_t session_attachment_encrypt_file( + const unsigned char* seed, + const char* filename, + ATTACHMENT_DOMAIN domain, + unsigned char* key_out, + unsigned char* (*make_buffer)(size_t, void* ctx), + void* ctx, + char* error) { + + try { + size_t enc_size = 0; + auto key = attachment::encrypt( + std::span{reinterpret_cast(seed), 32}, + std::filesystem::path{filename}, + static_cast(domain), + [make_buffer, ctx, &enc_size](size_t s) { + auto* buf = make_buffer(s, ctx); + if (!buf) + throw std::runtime_error{ + "encryption failed: make_buffer function returned NULL"}; + assert(!enc_size); + enc_size = s; + return std::span{reinterpret_cast(buf), s}; + }, + /*allow_large=*/true); + assert(enc_size); + std::memcpy(key_out, key.data(), key.size()); + sodium_zero_buffer(key.data(), key.size()); + return enc_size; + } catch (const std::exception& e) { + set_error(error, e); + return 0; + } +} + +LIBSESSION_C_API size_t session_attachment_decrypt_file( + const char* file_in, + const unsigned char* key, + unsigned char* (*make_buffer)(size_t, void* ctx), + void* ctx, + char* error) { + + try { + return attachment::decrypt( + std::filesystem::path{file_in}, + std::span{ + reinterpret_cast(key), attachment::ENCRYPT_KEY_SIZE}, + [make_buffer, ctx](size_t s) { + auto* buf = make_buffer(s, ctx); + if (!buf) + throw std::runtime_error{ + "decryption failed: make_buffer function returned NULL"}; + return std::span{reinterpret_cast(buf), s}; + }); + } catch (const std::exception& e) { + set_error(error, e); + return std::numeric_limits::max(); + } +} + +LIBSESSION_C_API bool session_attachment_decrypt_to_file( + const unsigned char* data, + size_t datalen, + const unsigned char* key, + const char* file_out, + char* error) { + + try { + attachment::decrypt( + std::span{reinterpret_cast(data), datalen}, + std::span{ + reinterpret_cast(key), attachment::ENCRYPT_KEY_SIZE}, + std::filesystem::path{file_out}); + return true; + } catch (const std::exception& e) { + return set_error(error, e); + } +} + +LIBSESSION_C_API bool session_attachment_decrypt_file_to_file( + const char* file_in, const unsigned char* key, const char* file_out, char* error) { + + try { + attachment::decrypt( + std::filesystem::path{file_in}, + std::span{ + reinterpret_cast(key), attachment::ENCRYPT_KEY_SIZE}, + std::filesystem::path{file_out}); + return true; + } catch (const std::exception& e) { + return set_error(error, e); + } +} +} diff --git a/src/internal-util.hpp b/src/internal-util.hpp new file mode 100644 index 00000000..2525666a --- /dev/null +++ b/src/internal-util.hpp @@ -0,0 +1,22 @@ +#pragma once +#include +#include + +namespace session { + +// Used by various C APIs with false returns to write a caught exception message into an error +// buffer (if provided) on the way out. The error buffer is expected to have at least 256 bytes +// available (the exception message will be truncated if longer than 255). +inline bool set_error(char* error, const std::exception& e) { + if (error) { + std::string_view err{e.what()}; + if (err.size() > 255) + err.remove_suffix(err.size() - 255); + std::memcpy(error, err.data(), err.size()); + error[err.size()] = 0; + } + + return false; +} + +} // namespace session diff --git a/src/session_network.cpp b/src/session_network.cpp index eaa85de3..8cfdabab 100644 --- a/src/session_network.cpp +++ b/src/session_network.cpp @@ -20,6 +20,7 @@ #include #include +#include "internal-util.hpp" #include "session/blinding.hpp" #include "session/ed25519.hpp" #include "session/export.h" @@ -2824,17 +2825,6 @@ inline session::network::Network& unbox(network_object* network_) { return *static_cast(network_->internals); } -inline bool set_error(char* error, const std::exception& e) { - if (!error) - return false; - - std::string msg = e.what(); - if (msg.size() > 255) - msg.resize(255); - std::memcpy(error, msg.c_str(), msg.size() + 1); - return false; -} - } // namespace extern "C" { diff --git a/tests/test_attachment_encrypt.cpp b/tests/test_attachment_encrypt.cpp index 1f27485d..61d02843 100644 --- a/tests/test_attachment_encrypt.cpp +++ b/tests/test_attachment_encrypt.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include "utils.hpp" @@ -114,7 +116,10 @@ TEST_CASE("Attachment encryption/decryption -- key separation", "[attachments][k auto DATA_SIZE = GENERATE(0, 20, 100, 1000, 33333); auto seed = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; - auto seed2 = "8123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; + auto seed2 = GENERATE( + "1123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b, + "0123456789abcdef0123456789abcdef1123456789abcdef0123456789abcdef"_hex_b, + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde7"_hex_b); const auto data = make_data(DATA_SIZE); @@ -132,7 +137,7 @@ TEST_CASE("Attachment encryption/decryption -- key separation", "[attachments][d auto DATA_SIZE = GENERATE(0, 20, 100, 1000, 33333); - auto seed = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; + auto seed = "2123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; const auto data = make_data(DATA_SIZE); @@ -148,7 +153,7 @@ TEST_CASE("Attachment encryption/decryption -- key separation", "[attachments][d TEST_CASE("Attachment encryption/decryption -- content separation", "[attachments][content-sep]") { - auto seed = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; + auto seed = "3123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; const auto data = make_data(50000); auto data2 = data; @@ -172,7 +177,7 @@ TEST_CASE("Attachment Decryptor", "[attachments][decryptor]") { auto FEED_SIZE = GENERATE(1, 2, 41, 4096, 10000000000); - auto seed = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; + auto seed = "4123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; const auto data = make_data(DATA_SIZE); @@ -193,3 +198,167 @@ TEST_CASE("Attachment Decryptor", "[attachments][decryptor]") { REQUIRE(d.finalize()); CHECK(!!(decrypted == data)); } + +struct temp_data_file { + inline static int i = 1; + std::filesystem::path path = + std::filesystem::temp_directory_path() / + std::filesystem::path{"libsession-util-attachment-test-{}"_format(i++)}; + + ~temp_data_file() { + if (std::filesystem::exists(path)) + std::filesystem::remove(path); + } + + // Constructs a temp filename without actually creating the file + temp_data_file() = default; + + // Constructs a plaintext file with deterministic output based on its size: + explicit temp_data_file(int len) { + std::ofstream out; + out.exceptions(std::ios::failbit | std::ios::badbit); + out.open(path, std::ios::binary | std::ios::trunc); + for (int i = 0; i < len; i++) { + std::byte v{static_cast(i * 7 % 256)}; + out.write(reinterpret_cast(&v), 1); + } + } +}; + +TEST_CASE( + "Attachment encryption: plaintext file to encrypted buffer", + "[attachments][files][encrypt]") { + + auto DATA_SIZE = GENERATE(0, 1, 2, 10, 100, 1000, 2000, 4000, 4053, 4054, 261983, 10218286); + + auto expected_size = DATA_SIZE < 4054 ? 4096 + : DATA_SIZE == 4054 ? 8192 + : DATA_SIZE == 261983 ? 270336 + : DATA_SIZE == 10218286 ? 10223616 + : -1; + + auto seed = "5123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; + + temp_data_file f{DATA_SIZE}; + + auto [enc, key] = attachment::encrypt(seed, f.path, attachment::Domain::ATTACHMENT); + CHECK(enc.size() == expected_size); + auto decr = attachment::decrypt(enc, key); + CHECK(!!(decr == make_data(DATA_SIZE))); +} + +static std::vector slurp_file(const std::filesystem::path& filename) { + std::ifstream in; + in.exceptions(std::ios::failbit | std::ios::badbit); + in.open(filename, std::ios::binary | std::ios::ate); + auto endpos = in.tellg(); + in.seekg(0, std::ios::beg); + auto size = endpos - in.tellg(); + + std::vector contents; + contents.resize(size); + in.read(reinterpret_cast(contents.data()), contents.size()); + + return contents; +} + +TEST_CASE( + "Attachment encryption: plaintext buffer to encrypted file", + "[attachments][files][encrypt]") { + + auto DATA_SIZE = GENERATE(0, 1, 2, 10, 100, 1000, 2000, 4000, 4053, 4054, 261983, 10218286); + + auto expected_size = DATA_SIZE < 4054 ? 4096 + : DATA_SIZE == 4054 ? 8192 + : DATA_SIZE == 261983 ? 270336 + : DATA_SIZE == 10218286 ? 10223616 + : -1; + + auto seed = "6123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; + + auto data = make_data(DATA_SIZE); + temp_data_file f; + + auto key = attachment::encrypt(seed, data, attachment::Domain::ATTACHMENT, f.path); + auto enc = slurp_file(f.path); + CHECK(enc.size() == expected_size); + auto decr = attachment::decrypt(enc, key); + CHECK(!!(decr == data)); +} + +TEST_CASE( + "Attachment decryption: encrypted buffer to plaintext file", + "[attachments][files][decrypt]") { + + auto DATA_SIZE = GENERATE(0, 1, 2, 10, 100, 1000, 2000, 4000, 4053, 4054, 261983, 10218286); + + auto expected_size = DATA_SIZE < 4054 ? 4096 + : DATA_SIZE == 4054 ? 8192 + : DATA_SIZE == 261983 ? 270336 + : DATA_SIZE == 10218286 ? 10223616 + : -1; + + auto seed = "7123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; + + const auto data = make_data(DATA_SIZE); + auto [enc, key] = attachment::encrypt(seed, data, attachment::Domain::ATTACHMENT); + + temp_data_file out{}; + + attachment::decrypt(enc, key, out.path); + + auto contents = slurp_file(out.path); + CHECK(contents.size() == data.size()); + CHECK(!!(contents == data)); +} + +TEST_CASE( + "Attachment decryption: encrypted file to plaintext buffer", + "[attachments][files][decrypt]") { + + auto DATA_SIZE = GENERATE(0, 1, 2, 10, 100, 1000, 2000, 4000, 4053, 4054, 261983, 10218286); + + auto expected_size = DATA_SIZE < 4054 ? 4096 + : DATA_SIZE == 4054 ? 8192 + : DATA_SIZE == 261983 ? 270336 + : DATA_SIZE == 10218286 ? 10223616 + : -1; + + auto seed = "8123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; + + const auto data = make_data(DATA_SIZE); + + temp_data_file out; + auto key = attachment::encrypt(seed, data, attachment::Domain::ATTACHMENT, out.path); + + auto decrypted = attachment::decrypt(out.path, key); + + CHECK(decrypted.size() == data.size()); + CHECK(!!(decrypted == data)); +} + +TEST_CASE( + "Attachment decryption: encrypted file to plaintext file", + "[attachments][files][decrypt]") { + + auto DATA_SIZE = GENERATE(0, 1, 2, 10, 100, 1000, 2000, 4000, 4053, 4054, 261983, 10218286); + + auto expected_size = DATA_SIZE < 4054 ? 4096 + : DATA_SIZE == 4054 ? 8192 + : DATA_SIZE == 261983 ? 270336 + : DATA_SIZE == 10218286 ? 10223616 + : -1; + + auto seed = "9123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; + + const auto data = make_data(DATA_SIZE); + + temp_data_file out_enc, out_dec; + auto key = attachment::encrypt(seed, data, attachment::Domain::ATTACHMENT, out_enc.path); + + attachment::decrypt(out_enc.path, key, out_dec.path); + + auto contents = slurp_file(out_dec.path); + CHECK(contents.size() == data.size()); + CHECK(!!(contents == data)); +} From 701c185f1efb480c3017017486664a44fdca5029 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 9 Oct 2025 21:23:14 -0300 Subject: [PATCH 3/3] Fix attachment file API bugs Reading was not working properly, thanks to C++'s horrific std::ifstream API. Next time I'm sticking to C APIs for I/O because yuck. --- src/attachments.cpp | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/attachments.cpp b/src/attachments.cpp index 6efb8c89..ef437419 100644 --- a/src/attachments.cpp +++ b/src/attachments.cpp @@ -298,7 +298,7 @@ std::array encrypt( bool allow_large) { std::ifstream in; - in.exceptions(std::ios::failbit | std::ios::badbit); + in.exceptions(std::ios::badbit); in.open(file, std::ios::binary | std::ios::ate); size_t size = in.tellg(); in.seekg(0, std::ios::beg); @@ -315,18 +315,25 @@ std::array encrypt( size_t in_size = 0; std::array chunk; - while (auto sz = in.readsome(reinterpret_cast(chunk.data()), chunk.size())) { + while (in.read(reinterpret_cast(chunk.data()), chunk.size())) { crypto_generichash_blake2b_update( - &b_st, reinterpret_cast(chunk.data()), sz); - in_size += sz; + &b_st, reinterpret_cast(chunk.data()), chunk.size()); + in_size += chunk.size(); } + if (in.gcount() > 0) { + crypto_generichash_blake2b_update( + &b_st, reinterpret_cast(chunk.data()), in.gcount()); + in_size += in.gcount(); + } + crypto_generichash_blake2b_final(&b_st, nonce_key.data(), nonce_key.size()); std::array key; std::memcpy(key.data(), nonce_key.data() + ENCRYPT_HEADER, ENCRYPT_KEY_SIZE); - in.seekg(0, std::ios::beg); in.clear(); + in.exceptions(std::ios::badbit | std::ios::failbit); + in.seekg(0, std::ios::beg); auto encrypted = make_buffer(size); if (encrypted.size() != size) @@ -799,7 +806,7 @@ size_t decrypt( std::function(size_t dec_size)> make_buffer) { std::ifstream in; - in.exceptions(std::ios::failbit | std::ios::badbit); + in.exceptions(std::ios::badbit); in.open(encrypted_file, std::ios::binary | std::ios::ate); size_t size = in.tellg(); in.seekg(0, std::ios::beg); @@ -825,8 +832,11 @@ size_t decrypt( }}; std::array chunk; - while (auto sz = in.readsome(reinterpret_cast(chunk.data()), chunk.size())) - d.update(std::span{chunk}.first(sz)); + while (in.read(reinterpret_cast(chunk.data()), chunk.size())) + d.update(chunk); + if (in.gcount() > 0) + d.update(std::span{chunk}.first(in.gcount())); + d.finalize(); return decrypted - out.begin(); @@ -852,7 +862,7 @@ void decrypt( try { std::ifstream in; - in.exceptions(std::ios::failbit | std::ios::badbit); + in.exceptions(std::ios::badbit); in.open(file_in, std::ios::binary | std::ios::ate); size_t size = in.tellg(); in.seekg(0, std::ios::beg); @@ -871,8 +881,10 @@ void decrypt( }}; std::array chunk; - while (auto sz = in.readsome(reinterpret_cast(chunk.data()), chunk.size())) - d.update(std::span{chunk}.first(sz)); + while (in.read(reinterpret_cast(chunk.data()), chunk.size())) + d.update(chunk); + if (in.gcount() > 0) + d.update(std::span{chunk}.first(in.gcount())); d.finalize(); } catch (const std::exception& e) { std::error_code ec;