diff --git a/src/core/passwords.cpp b/src/core/passwords.cpp index 4042e92c49..43601a5c98 100644 --- a/src/core/passwords.cpp +++ b/src/core/passwords.cpp @@ -172,6 +172,44 @@ String encryptString(String &plaintext, const String &password_str) { return out; } +String encryptFileHeader() { + // Same header encryptString() writes, up to (and including) the "Data: " prefix. + // The hex payload that follows may be appended incrementally across chunks. + String out = "Filetype: Bruce Encrypted File\nVersion: 1\n"; + out += "Algo: XOR\n"; + out += "KeyDerivationAlgo: MD5\n"; + out += "KeyDerivationPasses: 10\n"; + out += "Data: "; + return out; +} + +String encryptChunkToHex(const uint8_t *data, size_t len, const String &password_str, size_t streamOffset) { + // Derive the same 16-byte key xorEncryptDecryptMD5() uses (MD5 of the password, 10 + // passes), so a chunk-encrypted file decrypts identically to a single-shot one. + MD5Builder md5; + String hash = password_str; + for (int i = 0; i < 10; i++) { + md5.begin(); + md5.add(hash); + md5.calculate(); + } + uint8_t md5Hash[16]; + md5.getBytes(md5Hash); + + // Fixed-width "XX " per byte: the reader parses the Data line in 3-char groups, so + // every byte must be two uppercase hex digits followed by a separator. + static const char hexDigits[] = "0123456789ABCDEF"; + String out; + out.reserve(len * 3); + for (size_t i = 0; i < len; i++) { + uint8_t b = data[i] ^ md5Hash[(streamOffset + i) % 16]; + out += hexDigits[(b >> 4) & 0x0F]; + out += hexDigits[b & 0x0F]; + out += ' '; + } + return out; +} + /* OLD: String decryptString(String& cypertext, const String& password_str) diff --git a/src/core/passwords.h b/src/core/passwords.h index 1eafaa0095..46fa393430 100644 --- a/src/core/passwords.h +++ b/src/core/passwords.h @@ -6,6 +6,13 @@ String encryptString(String &plaintext, const String &password_str); +// Streaming helpers for chunked encryption (e.g. web uploads). The XOR keystream is +// position-based, so a file can be encrypted chunk by chunk as long as each chunk knows +// its absolute offset in the plaintext. encryptFileHeader() emits the header once, +// ending with "Data: "; encryptChunkToHex() appends the hex bytes for one chunk. +String encryptFileHeader(); +String encryptChunkToHex(const uint8_t *data, size_t len, const String &password_str, size_t streamOffset); + String decryptString(String &cypertext, const String &password_str); String readDecryptedFile(FS &fs, String filepath); diff --git a/src/core/wifi/webInterface.cpp b/src/core/wifi/webInterface.cpp index 8685413193..81de584b60 100644 --- a/src/core/wifi/webInterface.cpp +++ b/src/core/wifi/webInterface.cpp @@ -238,20 +238,20 @@ void handleUpload( // Serial.println("Failed to open file for writing: " + uploadFolder + "/" + filename); goto RETRY; } + if (request->hasArg("password") && request->_tempFile) { + // Write the encrypted-file header once; the hex payload is streamed per chunk. + String header = encryptFileHeader(); + request->_tempFile.write((const uint8_t *)header.c_str(), header.length()); + } } if (len) { if (request->hasArg("password")) { - // encryption requested - static int chunck_no = 0; - if (chunck_no != 0) { - // TODO: handle multiple chunks - request->send(404, "text/html", "file is too big"); - return; - } else chunck_no += 1; + // Encrypt this chunk incrementally. The XOR keystream is position-based, so + // `index` (the chunk's offset in the plaintext) keeps it continuous across + // chunks without buffering the whole file in RAM. String enc_password = request->arg("password"); - String plaintext = String((char *)data).substring(0, len); - String cyphertxt = encryptString(plaintext, enc_password); + String cyphertxt = encryptChunkToHex(data, len, enc_password, index); if (cyphertxt == "") { return; } if (request->_tempFile) request->_tempFile.write((const uint8_t *)cyphertxt.c_str(), cyphertxt.length()); @@ -260,6 +260,8 @@ void handleUpload( } } if (final) { + // Terminate the encrypted Data line before closing, matching encryptString(). + if (request->hasArg("password") && request->_tempFile) request->_tempFile.write((const uint8_t *)"\n", 1); // close the file handle as the upload is now done if (request->_tempFile) request->_tempFile.close(); }