Skip to content

Commit b7dd346

Browse files
committed
Pull through file/streaming encrypt+decrypt (and unmanaged of the same)
1 parent 9e75e5d commit b7dd346

6 files changed

Lines changed: 331 additions & 5 deletions

File tree

Cargo.lock

Lines changed: 34 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ bindgen = "0.72"
1010
cfg-if = "1"
1111
env_logger = { version = "0.11", default-features = false }
1212
flapigen = { git = "https://github.com/Dushistov/flapigen-rs.git", rev = "49a59f68" }
13-
ironoxide = { version = "4.2", features = [
13+
ironoxide = { path = "../ironoxide", features = [
1414
"blocking",
1515
"beta",
1616
"tls-rustls",

common/lib.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1411,3 +1411,113 @@ mod blind_index_search {
14111411
.collect())
14121412
}
14131413
}
1414+
1415+
mod document_file_encrypt_result {
1416+
use super::*;
1417+
1418+
pub fn id(d: &DocumentFileEncryptResult) -> DocumentId {
1419+
d.id().clone()
1420+
}
1421+
pub fn name(d: &DocumentFileEncryptResult) -> Option<DocumentName> {
1422+
d.name().cloned()
1423+
}
1424+
pub fn created(d: &DocumentFileEncryptResult) -> OffsetDateTime {
1425+
*d.created()
1426+
}
1427+
pub fn last_updated(d: &DocumentFileEncryptResult) -> OffsetDateTime {
1428+
*d.last_updated()
1429+
}
1430+
}
1431+
1432+
impl document_access_change_result::DocumentAccessChange for DocumentFileEncryptResult {
1433+
fn changed(&self) -> document_access_change_result::SucceededResult {
1434+
document_access_change_result::to_succeeded_result(self.grants())
1435+
}
1436+
1437+
fn errors(&self) -> document_access_change_result::FailedResult {
1438+
document_access_change_result::to_failed_result(self.access_errs())
1439+
}
1440+
}
1441+
1442+
mod document_file_encrypt_unmanaged_result {
1443+
use super::*;
1444+
1445+
pub fn id(d: &DocumentFileEncryptUnmanagedResult) -> DocumentId {
1446+
d.id().clone()
1447+
}
1448+
pub fn encrypted_deks(d: &DocumentFileEncryptUnmanagedResult) -> Vec<i8> {
1449+
u8_conv(d.encrypted_deks()).to_vec()
1450+
}
1451+
}
1452+
1453+
impl document_access_change_result::DocumentAccessChange for DocumentFileEncryptUnmanagedResult {
1454+
fn changed(&self) -> document_access_change_result::SucceededResult {
1455+
document_access_change_result::to_succeeded_result(self.grants())
1456+
}
1457+
1458+
fn errors(&self) -> document_access_change_result::FailedResult {
1459+
document_access_change_result::to_failed_result(self.access_errs())
1460+
}
1461+
}
1462+
1463+
mod document_file_decrypt_result {
1464+
use super::*;
1465+
1466+
pub fn id(d: &DocumentFileDecryptResult) -> DocumentId {
1467+
d.id().clone()
1468+
}
1469+
pub fn name(d: &DocumentFileDecryptResult) -> Option<DocumentName> {
1470+
d.name().cloned()
1471+
}
1472+
}
1473+
1474+
mod document_file_decrypt_unmanaged_result {
1475+
use super::*;
1476+
use crate::document_decrypt_unmanaged_result::UserOrGroupId;
1477+
1478+
pub fn id(d: &DocumentFileDecryptUnmanagedResult) -> DocumentId {
1479+
d.id().clone()
1480+
}
1481+
pub fn access_via(d: &DocumentFileDecryptUnmanagedResult) -> UserOrGroupId {
1482+
let (id, is_user) = match d.access_via() {
1483+
UserOrGroup::User { id } => (id.id().to_string(), true),
1484+
UserOrGroup::Group { id } => (id.id().to_string(), false),
1485+
};
1486+
UserOrGroupId::new(id, is_user)
1487+
}
1488+
}
1489+
1490+
fn document_file_encrypt(
1491+
sdk: &IronOxide,
1492+
source_path: &str,
1493+
destination_path: &str,
1494+
opts: &DocumentEncryptOpts,
1495+
) -> Result<DocumentFileEncryptResult, String> {
1496+
Ok(sdk.document_file_encrypt(source_path, destination_path, opts)?)
1497+
}
1498+
1499+
fn document_file_decrypt(
1500+
sdk: &IronOxide,
1501+
source_path: &str,
1502+
destination_path: &str,
1503+
) -> Result<DocumentFileDecryptResult, String> {
1504+
Ok(sdk.document_file_decrypt(source_path, destination_path)?)
1505+
}
1506+
1507+
fn document_file_encrypt_unmanaged(
1508+
sdk: &IronOxide,
1509+
source_path: &str,
1510+
destination_path: &str,
1511+
opts: &DocumentEncryptOpts,
1512+
) -> Result<DocumentFileEncryptUnmanagedResult, String> {
1513+
Ok(sdk.document_file_encrypt_unmanaged(source_path, destination_path, opts)?)
1514+
}
1515+
1516+
fn document_file_decrypt_unmanaged(
1517+
sdk: &IronOxide,
1518+
source_path: &str,
1519+
destination_path: &str,
1520+
encrypted_deks: &[i8],
1521+
) -> Result<DocumentFileDecryptUnmanagedResult, String> {
1522+
Ok(sdk.document_file_decrypt_unmanaged(source_path, destination_path, i8_conv(encrypted_deks))?)
1523+
}

common/lib.rs.in

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,66 @@ class DocumentAccessResult {
757757
pre_build_generate_equals_and_hashcode DocumentAccessResult;
758758
});
759759

760+
foreign_class!(
761+
/// Result of file encryption (managed). Contains metadata about the encrypted document.
762+
class DocumentFileEncryptResult {
763+
self_type DocumentFileEncryptResult;
764+
private constructor = empty;
765+
/// Unique (within the segment) id of the document
766+
fn document_file_encrypt_result::id(&self) -> DocumentId; alias getId;
767+
/// Non-unique document name
768+
fn document_file_encrypt_result::name(&self) -> Option<DocumentName>; alias getName;
769+
/// When the document was created
770+
fn document_file_encrypt_result::created(&self) -> OffsetDateTime; alias getCreated;
771+
/// When the document was last updated
772+
fn document_file_encrypt_result::last_updated(&self) -> OffsetDateTime; alias getLastUpdated;
773+
/// Get the users and groups whose access was successfully granted
774+
fn DocumentAccessChange::changed(&self) -> SucceededResult; alias getChanged;
775+
/// Get the users and groups whose access failed to be granted
776+
fn DocumentAccessChange::errors(&self) -> FailedResult; alias getErrors;
777+
pre_build_generate_equals_and_hashcode DocumentFileEncryptResult;
778+
});
779+
780+
foreign_class!(
781+
/// Result of file encryption (unmanaged). Contains the document ID and encrypted DEKs.
782+
class DocumentFileEncryptUnmanagedResult {
783+
self_type DocumentFileEncryptUnmanagedResult;
784+
private constructor = empty;
785+
/// Unique (within the segment) id of the document
786+
fn document_file_encrypt_unmanaged_result::id(&self) -> DocumentId; alias getId;
787+
/// Bytes of encrypted document encryption keys (EDEKs)
788+
fn document_file_encrypt_unmanaged_result::encrypted_deks(&self) -> Vec<i8>; alias getEncryptedDeks;
789+
/// Get the users and groups whose access was successfully granted
790+
fn DocumentAccessChange::changed(&self) -> SucceededResult; alias getChanged;
791+
/// Get the users and groups whose access failed to be granted
792+
fn DocumentAccessChange::errors(&self) -> FailedResult; alias getErrors;
793+
pre_build_generate_equals_and_hashcode DocumentFileEncryptUnmanagedResult;
794+
});
795+
796+
foreign_class!(
797+
/// Result of file decryption (managed). Contains minimal metadata about the decrypted document.
798+
class DocumentFileDecryptResult {
799+
self_type DocumentFileDecryptResult;
800+
private constructor = empty;
801+
/// Unique (within the segment) id of the document
802+
fn document_file_decrypt_result::id(&self) -> DocumentId; alias getId;
803+
/// Non-unique document name
804+
fn document_file_decrypt_result::name(&self) -> Option<DocumentName>; alias getName;
805+
pre_build_generate_equals_and_hashcode DocumentFileDecryptResult;
806+
});
807+
808+
foreign_class!(
809+
/// Result of file decryption (unmanaged). Contains the document ID and access info.
810+
class DocumentFileDecryptUnmanagedResult {
811+
self_type DocumentFileDecryptUnmanagedResult;
812+
private constructor = empty;
813+
/// Unique (within the segment) id of the document
814+
fn document_file_decrypt_unmanaged_result::id(&self) -> DocumentId; alias getId;
815+
/// User/Group that granted access to the encrypted data
816+
fn document_file_decrypt_unmanaged_result::access_via(&self) -> UserOrGroupId; alias getAccessViaUserOrGroup;
817+
pre_build_generate_equals_and_hashcode DocumentFileDecryptUnmanagedResult;
818+
});
819+
760820
foreign_class!(
761821
/// Policy evaluation caching config
762822
///
@@ -1066,6 +1126,41 @@ class IronOxide {
10661126
/// @return result containing updated EDEKs and per-user/group success/failure
10671127
fn document_revoke_access_unmanaged(&self, edeks: &[i8], userRevokes: &[UserId], groupRevokes: &[GroupId])
10681128
-> Result<DocumentAccessUnmanagedResult, String>; alias documentRevokeAccessUnmanaged;
1129+
/// Encrypt a file from source path to destination path (managed).
1130+
/// Uses streaming I/O with constant memory usage. The output format is identical to documentEncrypt.
1131+
///
1132+
/// @param sourcePath path to the plaintext file to encrypt
1133+
/// @param destinationPath path where the encrypted file will be written
1134+
/// @param encryptOpts optional document encrypt parameters
1135+
/// @return metadata about the encrypted document including id, name, timestamps, and access grants/errors
1136+
fn document_file_encrypt(&self, sourcePath: &str, destinationPath: &str, encryptOpts: &DocumentEncryptOpts)
1137+
-> Result<DocumentFileEncryptResult, String>; alias documentFileEncrypt;
1138+
/// Decrypt an encrypted file to destination path (managed).
1139+
/// Uses streaming I/O with constant memory usage.
1140+
///
1141+
/// @param sourcePath path to the encrypted file to decrypt
1142+
/// @param destinationPath path where the decrypted file will be written
1143+
/// @return metadata about the decrypted document including id and name
1144+
fn document_file_decrypt(&self, sourcePath: &str, destinationPath: &str)
1145+
-> Result<DocumentFileDecryptResult, String>; alias documentFileDecrypt;
1146+
/// Encrypt a file from source path to destination path (unmanaged).
1147+
/// Uses streaming I/O with constant memory usage. Returns encrypted DEKs instead of storing them on the server.
1148+
///
1149+
/// @param sourcePath path to the plaintext file to encrypt
1150+
/// @param destinationPath path where the encrypted file will be written
1151+
/// @param encryptOpts optional document encrypt parameters
1152+
/// @return document ID, encrypted DEKs, and access grants/errors
1153+
fn document_file_encrypt_unmanaged(&self, sourcePath: &str, destinationPath: &str, encryptOpts: &DocumentEncryptOpts)
1154+
-> Result<DocumentFileEncryptUnmanagedResult, String>; alias documentFileEncryptUnmanaged;
1155+
/// Decrypt an encrypted file to destination path (unmanaged).
1156+
/// Uses streaming I/O with constant memory usage. Caller provides encrypted DEKs.
1157+
///
1158+
/// @param sourcePath path to the encrypted file to decrypt
1159+
/// @param destinationPath path where the decrypted file will be written
1160+
/// @param encryptedDeks encrypted document encryption keys
1161+
/// @return document ID and the user/group that granted access
1162+
fn document_file_decrypt_unmanaged(&self, sourcePath: &str, destinationPath: &str, encryptedDeks: &[i8])
1163+
-> Result<DocumentFileDecryptUnmanagedResult, String>; alias documentFileDecryptUnmanaged;
10691164
/// Initialize IronOxide with a device and a pre-populated public key cache.
10701165
///
10711166
/// @param init device context used to initialize the IronOxide with a set of device keys

cpp/test/test.cpp

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#include "acutest.h"
22
#include <cstdlib>
3+
#include <cstdio>
4+
#include <fstream>
35
#include <iostream>
46
#include <random>
57
#include "IronOxide.hpp"
@@ -273,6 +275,46 @@ void export_reimport_public_key_cache(void)
273275
TEST_CHECK_(doc_list.getResult().as_slice().size() >= 0, "Should be able to list documents after reinit with cache.");
274276
}
275277

278+
void file_encrypt_decrypt_unmanaged_roundtrip(void)
279+
{
280+
DeviceContext d = unwrap(DeviceContext::fromJsonString(deviceContextString));
281+
IronOxide sdk = unwrap(IronOxide::initialize(d, IronOxideConfig()));
282+
283+
// Create temp file paths
284+
std::string source_path = "/tmp/ironoxide_cpp_test_source.txt";
285+
std::string encrypted_path = "/tmp/ironoxide_cpp_test_encrypted.iron";
286+
std::string decrypted_path = "/tmp/ironoxide_cpp_test_decrypted.txt";
287+
288+
// Write test data to source file
289+
std::string test_data = "Hello, streaming encryption from C++!";
290+
{
291+
std::ofstream out(source_path, std::ios::binary);
292+
out.write(test_data.data(), test_data.size());
293+
}
294+
295+
// Encrypt file
296+
auto encrypt_result = unwrap(sdk.documentFileEncryptUnmanaged(source_path.c_str(), encrypted_path.c_str(), DocumentEncryptOpts()));
297+
TEST_CHECK_(encrypt_result.getId().getId().to_std_string().length() == 32, "Document ID should be 32 chars.");
298+
TEST_CHECK_(encrypt_result.getEncryptedDeks().size() > 0, "Encrypted DEKs should be non-empty.");
299+
300+
// Decrypt file
301+
auto decrypt_result = unwrap(sdk.documentFileDecryptUnmanaged(
302+
encrypted_path.c_str(),
303+
decrypted_path.c_str(),
304+
vec_to_slice(encrypt_result.getEncryptedDeks())));
305+
TEST_CHECK_(decrypt_result.getId() == encrypt_result.getId(), "Document IDs should match.");
306+
307+
// Read decrypted file and verify contents
308+
std::ifstream in(decrypted_path, std::ios::binary);
309+
std::string decrypted_data((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
310+
TEST_CHECK_(decrypted_data == test_data, "Decrypted file content should match original.");
311+
312+
// Cleanup temp files
313+
std::remove(source_path.c_str());
314+
std::remove(encrypted_path.c_str());
315+
std::remove(decrypted_path.c_str());
316+
}
317+
276318
TEST_LIST = {
277319
{"test_user_id", test_user_id},
278320
{"test_user_id_error", test_user_id_error},
@@ -288,4 +330,5 @@ TEST_LIST = {
288330
{"unmanaged_metadata_and_id", unmanaged_metadata_and_id},
289331
{"unmanaged_grant_access", unmanaged_grant_access},
290332
{"export_reimport_public_key_cache", export_reimport_public_key_cache},
333+
{"file_encrypt_decrypt_unmanaged_roundtrip", file_encrypt_decrypt_unmanaged_roundtrip},
291334
{NULL, NULL}};

0 commit comments

Comments
 (0)