diff --git a/include/opendht/crypto.h b/include/opendht/crypto.h
index 60f2a069cfbc2af80333c9b05a328d289ee66034..c93129cd936f9308cf7ba2837dd96748c1b8ce70 100644
--- a/include/opendht/crypto.h
+++ b/include/opendht/crypto.h
@@ -802,6 +802,12 @@ OPENDHT_PUBLIC Blob aesEncrypt(const uint8_t* data, size_t data_length, const Bl
 OPENDHT_PUBLIC inline Blob aesEncrypt(const Blob& data, const Blob& key) {
     return aesEncrypt(data.data(), data.size(), key);
 }
+/**
+ * AES-GCM encryption with argon2 key derivation.
+ * This function uses `stretchKey` to generate an AES key from the password and a random salt.
+ * The result is a bundle including the salt that can be decrypted with `aesDecrypt(data, password)`.
+ * If needed, the salt or encrypted data can be individually extracted from the bundle with `aesGetSalt` and `aesGetEncrypted`.
+ */
 OPENDHT_PUBLIC Blob aesEncrypt(const Blob& data, std::string_view password);
 
 /**
@@ -833,6 +839,18 @@ OPENDHT_PUBLIC std::string_view inline aesGetEncrypted(const Blob& data) {
     return aesGetEncrypted(data.data(), data.size());
 }
 
+/** Build an encrypted bundle that can be decrypted with aesDecrypt(data, password).
+ *  @param encryptedData: result of `aesEncrypt(data, key)` or `aesGetEncrypted`
+ *  @param salt: should match the encryption key and password so that `stretchKey(password, salk) == key`.
+ *  Can be obtained from an existing bundle with `aesGetSalt`.
+ **/
+OPENDHT_PUBLIC Blob aesBuildEncrypted(const uint8_t* encryptedData, size_t data_length, const Blob& salt);
+OPENDHT_PUBLIC Blob inline aesBuildEncrypted(const Blob& encryptedData, const Blob& salt) {
+    return aesBuildEncrypted(encryptedData.data(), encryptedData.size(), salt);
+}
+OPENDHT_PUBLIC Blob inline aesBuildEncrypted(std::string_view encryptedData, const Blob& salt) {
+    return aesBuildEncrypted((const uint8_t*)encryptedData.data(), encryptedData.size(), salt);
+}
 
 }
 }
diff --git a/src/crypto.cpp b/src/crypto.cpp
index 49cbc9dea3fa237bde521a591ccefd8b03b55d6b..e097a2d9a68cc37ab055b18470475b9bd7bf3334 100644
--- a/src/crypto.cpp
+++ b/src/crypto.cpp
@@ -107,9 +107,7 @@ Blob aesEncrypt(const Blob& data, std::string_view password)
 {
     Blob salt;
     Blob key = stretchKey(password, salt, 256 / 8);
-    Blob encrypted = aesEncrypt(data, key);
-    encrypted.insert(encrypted.begin(), salt.begin(), salt.end());
-    return encrypted;
+    return aesBuildEncrypted(aesEncrypt(data, key), salt);
 }
 
 Blob aesDecrypt(const uint8_t* data, size_t data_length, const Blob& key)
@@ -174,6 +172,15 @@ std::string_view aesGetEncrypted(const uint8_t* data, size_t data_length)
     return std::string_view((const char*)(data+PASSWORD_SALT_LENGTH), data_length - PASSWORD_SALT_LENGTH);
 }
 
+Blob aesBuildEncrypted(const uint8_t* data, size_t data_length, const Blob& salt)
+{
+    Blob ret;
+    ret.reserve(data_length + salt.size());
+    ret.insert(ret.end(), salt.begin(), salt.end());
+    ret.insert(ret.end(), data, data + data_length);
+    return ret;
+}
+
 Blob aesGetKey(const uint8_t* data, size_t data_length, std::string_view password)
 {
     Blob salt = aesGetSalt(data, data_length);