1use crate::{
8 PolicyCache, Result,
9 config::IronOxideConfig,
10 crypto::{aes::AES_KEY_LEN, streaming, transform},
11 group::GroupId,
12 internal::{
13 IronOxideErr, PrivateKey, PublicKey, PublicKeyCache, RequestAuth,
14 document_api::{
15 self, DocAccessEditErr, DocumentHeader, DocumentId, DocumentName, UserOrGroup,
16 parse_header_length, recrypt_document,
17 requests::{self, document_create},
18 },
19 },
20 policy::PolicyGrant,
21 proto::transform::EncryptedDeks as EncryptedDeksP,
22 user::UserId,
23};
24use protobuf::Message;
25use rand::CryptoRng;
26use recrypt::prelude::*;
27use std::{
28 fs::{self, File, OpenOptions},
29 io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write},
30 sync::Mutex,
31};
32use time::OffsetDateTime;
33
34fn create_output_file(path: &str) -> Result<File> {
38 #[cfg(unix)]
39 {
40 use std::os::unix::fs::OpenOptionsExt;
41 OpenOptions::new()
42 .write(true)
43 .create(true)
44 .truncate(true)
45 .mode(0o600)
46 .open(path)
47 .map_err(|e| IronOxideErr::FileIoError {
48 path: Some(path.to_string()),
49 operation: "create".into(),
50 message: e.to_string(),
51 })
52 }
53
54 #[cfg(windows)]
55 {
56 use std::os::windows::fs::OpenOptionsExt;
57 OpenOptions::new()
58 .write(true)
59 .create(true)
60 .truncate(true)
61 .share_mode(0)
62 .open(path)
63 .map_err(|e| IronOxideErr::FileIoError {
64 path: Some(path.to_string()),
65 operation: "create".into(),
66 message: e.to_string(),
67 })
68 }
69
70 #[cfg(not(any(unix, windows)))]
71 {
72 OpenOptions::new()
73 .write(true)
74 .create(true)
75 .truncate(true)
76 .open(path)
77 .map_err(|e| IronOxideErr::FileIoError {
78 path: Some(path.to_string()),
79 operation: "create".into(),
80 message: e.to_string(),
81 })
82 }
83}
84
85fn reset_file_permissions(path: &str) -> Result<()> {
88 #[cfg(unix)]
89 {
90 use fs::Permissions;
91 use std::os::unix::fs::PermissionsExt;
92 fs::set_permissions(path, Permissions::from_mode(0o644)).map_err(|e| {
93 IronOxideErr::FileIoError {
94 path: Some(path.to_string()),
95 operation: "set_permissions".into(),
96 message: e.to_string(),
97 }
98 })
99 }
100
101 #[cfg(not(unix))]
102 {
103 let _ = path;
105 Ok(())
106 }
107}
108
109struct CleanupOnDrop {
112 path: String,
113 committed: bool,
114}
115
116impl CleanupOnDrop {
117 fn new(path: &str) -> Self {
118 Self {
119 path: path.to_string(),
120 committed: false,
121 }
122 }
123
124 fn commit(mut self) {
126 self.committed = true;
127 }
128}
129
130impl Drop for CleanupOnDrop {
131 fn drop(&mut self) {
132 if !self.committed {
133 let _ = fs::remove_file(&self.path);
136 }
137 }
138}
139
140fn read_document_header(source_path: &str) -> Result<(DocumentHeader, File)> {
143 let mut source_file = File::open(source_path).map_err(|e| IronOxideErr::FileIoError {
144 path: Some(source_path.to_string()),
145 operation: "open".into(),
146 message: e.to_string(),
147 })?;
148
149 let mut header_prefix = [0u8; 3];
151 source_file
152 .read_exact(&mut header_prefix)
153 .map_err(|e| IronOxideErr::FileIoError {
154 path: Some(source_path.to_string()),
155 operation: "read_header".into(),
156 message: e.to_string(),
157 })?;
158
159 let header_len = parse_header_length(&header_prefix)?;
160
161 source_file
163 .seek(SeekFrom::Start(0))
164 .map_err(|e| IronOxideErr::FileIoError {
165 path: Some(source_path.to_string()),
166 operation: "seek".into(),
167 message: e.to_string(),
168 })?;
169
170 let mut header_bytes = vec![0u8; header_len];
171 source_file
172 .read_exact(&mut header_bytes)
173 .map_err(|e| IronOxideErr::FileIoError {
174 path: Some(source_path.to_string()),
175 operation: "read_header".into(),
176 message: e.to_string(),
177 })?;
178
179 let doc_header: DocumentHeader =
181 serde_json::from_slice(&header_bytes[3..header_len]).map_err(|_| {
182 IronOxideErr::DocumentHeaderParseFailure(
183 "Unable to parse document header. Header value is corrupted.".to_string(),
184 )
185 })?;
186
187 Ok((doc_header, source_file))
188}
189
190fn stream_decrypt_to_file(
194 key_bytes: &[u8; AES_KEY_LEN],
195 source_file: &File,
196 destination_path: &str,
197) -> Result<()> {
198 let mut output_file = create_output_file(destination_path)?;
199 let cleanup_guard = CleanupOnDrop::new(destination_path);
200
201 let mut reader = BufReader::new(source_file);
202 let mut writer = BufWriter::new(&mut output_file);
203
204 streaming::decrypt_stream(key_bytes, &mut reader, &mut writer)?;
205
206 writer.flush().map_err(|e| IronOxideErr::FileIoError {
207 path: Some(destination_path.to_string()),
208 operation: "flush".into(),
209 message: e.to_string(),
210 })?;
211
212 cleanup_guard.commit();
214 reset_file_permissions(destination_path)?;
215
216 Ok(())
217}
218
219#[derive(Clone, Debug, Eq, Hash, PartialEq)]
223pub struct DocumentFileEncryptResult {
224 id: DocumentId,
225 name: Option<DocumentName>,
226 created: OffsetDateTime,
227 updated: OffsetDateTime,
228 grants: Vec<UserOrGroup>,
229 access_errs: Vec<DocAccessEditErr>,
230}
231
232impl DocumentFileEncryptResult {
233 pub fn id(&self) -> &DocumentId {
235 &self.id
236 }
237
238 pub fn name(&self) -> Option<&DocumentName> {
240 self.name.as_ref()
241 }
242
243 pub fn created(&self) -> &OffsetDateTime {
245 &self.created
246 }
247
248 pub fn last_updated(&self) -> &OffsetDateTime {
250 &self.updated
251 }
252 pub fn grants(&self) -> &[UserOrGroup] {
254 &self.grants
255 }
256
257 pub fn access_errs(&self) -> &[DocAccessEditErr] {
259 &self.access_errs
260 }
261}
262
263#[derive(Clone, Debug, Eq, Hash, PartialEq)]
267pub struct DocumentFileEncryptUnmanagedResult {
268 id: DocumentId,
269 encrypted_deks: Vec<u8>,
270 grants: Vec<UserOrGroup>,
271 access_errs: Vec<DocAccessEditErr>,
272}
273
274impl DocumentFileEncryptUnmanagedResult {
275 pub fn id(&self) -> &DocumentId {
277 &self.id
278 }
279
280 pub fn encrypted_deks(&self) -> &[u8] {
282 &self.encrypted_deks
283 }
284
285 pub fn grants(&self) -> &[UserOrGroup] {
287 &self.grants
288 }
289
290 pub fn access_errs(&self) -> &[DocAccessEditErr] {
292 &self.access_errs
293 }
294}
295
296#[derive(Clone, Debug, Eq, Hash, PartialEq)]
300pub struct DocumentFileDecryptResult {
301 id: DocumentId,
302 name: Option<DocumentName>,
303}
304
305impl DocumentFileDecryptResult {
306 pub fn id(&self) -> &DocumentId {
308 &self.id
309 }
310
311 pub fn name(&self) -> Option<&DocumentName> {
313 self.name.as_ref()
314 }
315}
316
317#[derive(Clone, Debug, Eq, Hash, PartialEq)]
321pub struct DocumentFileDecryptUnmanagedResult {
322 id: DocumentId,
323 access_via: UserOrGroup,
324}
325
326impl DocumentFileDecryptUnmanagedResult {
327 pub fn id(&self) -> &DocumentId {
329 &self.id
330 }
331
332 pub fn access_via(&self) -> &UserOrGroup {
334 &self.access_via
335 }
336}
337
338pub async fn encrypt_file_to_path<R1, R2>(
341 auth: &RequestAuth,
342 config: &IronOxideConfig,
343 recrypt: &Recrypt<Sha256, Ed25519, RandomBytes<R1>>,
344 user_master_pub_key: &PublicKey,
345 rng: &Mutex<R2>,
346 source_path: &str,
347 destination_path: &str,
348 document_id: Option<DocumentId>,
349 document_name: Option<DocumentName>,
350 grant_to_author: bool,
351 user_grants: &[UserId],
352 group_grants: &[GroupId],
353 policy_grant: Option<&PolicyGrant>,
354 policy_cache: &PolicyCache,
355 public_key_cache: &PublicKeyCache,
356) -> Result<DocumentFileEncryptResult>
357where
358 R1: CryptoRng,
359 R2: CryptoRng,
360{
361 let source_file = File::open(source_path).map_err(|e| IronOxideErr::FileIoError {
362 path: Some(source_path.to_string()),
363 operation: "open".into(),
364 message: e.to_string(),
365 })?;
366 let (dek, doc_sym_key) = transform::generate_new_doc_key(recrypt);
367 let doc_id = document_id.unwrap_or_else(|| DocumentId::goo_id(rng));
368 let (grants, key_errs) = document_api::resolve_keys_for_grants(
369 auth,
370 config,
371 user_grants,
372 group_grants,
373 policy_grant,
374 if grant_to_author {
375 Some(user_master_pub_key)
376 } else {
377 None
378 },
379 policy_cache,
380 public_key_cache,
381 )
382 .await?;
383 let mut output_file = create_output_file(destination_path)?;
384
385 let header = DocumentHeader::new(doc_id.clone(), auth.segment_id);
387 let header_bytes = header.pack();
388 output_file
389 .write_all(&header_bytes.0)
390 .map_err(|e| IronOxideErr::FileIoError {
391 path: Some(destination_path.to_string()),
392 operation: "write_header".into(),
393 message: e.to_string(),
394 })?;
395
396 let mut reader = BufReader::new(source_file);
397 let mut writer = BufWriter::new(&mut output_file);
398
399 let key_bytes = *doc_sym_key.bytes();
401 streaming::encrypt_stream(&key_bytes, rng, &mut reader, &mut writer)?;
402 reset_file_permissions(destination_path)?;
403
404 let recryption_result =
406 recrypt_document(&auth.signing_private_key, recrypt, dek, &doc_id, grants)?;
407
408 let create_result = document_create::document_create_request(
410 auth,
411 doc_id,
412 document_name,
413 recryption_result.edeks,
414 )
415 .await?;
416
417 Ok(DocumentFileEncryptResult {
418 id: create_result.id,
419 name: create_result.name,
420 created: create_result.created,
421 updated: create_result.updated,
422 grants: create_result
423 .shared_with
424 .into_iter()
425 .map(|sw| sw.into())
426 .collect(),
427 access_errs: [key_errs, recryption_result.encryption_errs].concat(),
428 })
429}
430
431pub async fn decrypt_file_to_path<CR>(
435 auth: &RequestAuth,
436 recrypt: std::sync::Arc<Recrypt<Sha256, Ed25519, RandomBytes<CR>>>,
437 device_private_key: &PrivateKey,
438 source_path: &str,
439 destination_path: &str,
440) -> Result<DocumentFileDecryptResult>
441where
442 CR: CryptoRng + Send + Sync + 'static,
443{
444 let (doc_header, source_file) = read_document_header(source_path)?;
445
446 let doc_meta = document_api::document_get_metadata(auth, &doc_header.document_id).await?;
448
449 let device_private_key = device_private_key.clone();
451 let encrypted_symmetric_key = doc_meta.to_encrypted_symmetric_key()?;
452
453 let sym_key = tokio::task::spawn_blocking(move || {
454 transform::decrypt_as_symmetric_key(
455 &recrypt,
456 encrypted_symmetric_key,
457 device_private_key.recrypt_key(),
458 )
459 })
460 .await??;
461
462 let key_bytes: [u8; AES_KEY_LEN] = *sym_key.bytes();
463 stream_decrypt_to_file(&key_bytes, &source_file, destination_path)?;
464
465 Ok(DocumentFileDecryptResult {
466 id: doc_meta.id().clone(),
467 name: doc_meta.name().cloned(),
468 })
469}
470
471pub async fn encrypt_file_unmanaged<R1, R2>(
473 auth: &RequestAuth,
474 config: &IronOxideConfig,
475 recrypt: &Recrypt<Sha256, Ed25519, RandomBytes<R1>>,
476 user_master_pub_key: &PublicKey,
477 rng: &Mutex<R2>,
478 source_path: &str,
479 destination_path: &str,
480 document_id: Option<DocumentId>,
481 grant_to_author: bool,
482 user_grants: &[UserId],
483 group_grants: &[GroupId],
484 policy_grant: Option<&PolicyGrant>,
485 policy_cache: &PolicyCache,
486 public_key_cache: &PublicKeyCache,
487) -> Result<DocumentFileEncryptUnmanagedResult>
488where
489 R1: CryptoRng,
490 R2: CryptoRng,
491{
492 let source_file = File::open(source_path).map_err(|e| IronOxideErr::FileIoError {
494 path: Some(source_path.to_string()),
495 operation: "open".into(),
496 message: e.to_string(),
497 })?;
498
499 let (dek, doc_sym_key) = transform::generate_new_doc_key(recrypt);
501 let doc_id = document_id.unwrap_or_else(|| DocumentId::goo_id(rng));
502
503 let (grants, key_errs) = document_api::resolve_keys_for_grants(
505 auth,
506 config,
507 user_grants,
508 group_grants,
509 policy_grant,
510 if grant_to_author {
511 Some(user_master_pub_key)
512 } else {
513 None
514 },
515 policy_cache,
516 public_key_cache,
517 )
518 .await?;
519
520 let mut output_file = create_output_file(destination_path)?;
522
523 let header = DocumentHeader::new(doc_id.clone(), auth.segment_id);
525 let header_bytes = header.pack();
526 output_file
527 .write_all(&header_bytes.0)
528 .map_err(|e| IronOxideErr::FileIoError {
529 path: Some(destination_path.to_string()),
530 operation: "write_header".into(),
531 message: e.to_string(),
532 })?;
533
534 let mut reader = BufReader::new(source_file);
536 let mut writer = BufWriter::new(&mut output_file);
537
538 let key_bytes: [u8; AES_KEY_LEN] = *doc_sym_key.bytes();
539 streaming::encrypt_stream(&key_bytes, rng, &mut reader, &mut writer)?;
540
541 let r = recrypt_document(&auth.signing_private_key, recrypt, dek, &doc_id, grants)?;
543
544 let edek_bytes = document_api::edeks_to_bytes(&r.edeks, &doc_id, auth.segment_id)?;
546
547 let successful_grants: Vec<UserOrGroup> =
548 r.edeks.iter().map(|edek| edek.grant_to().clone()).collect();
549 let all_errs: Vec<DocAccessEditErr> = key_errs
550 .into_iter()
551 .chain(r.encryption_errs.clone())
552 .collect();
553
554 reset_file_permissions(destination_path)?;
556
557 Ok(DocumentFileEncryptUnmanagedResult {
558 id: doc_id,
559 encrypted_deks: edek_bytes,
560 grants: successful_grants,
561 access_errs: all_errs,
562 })
563}
564
565pub async fn decrypt_file_unmanaged<CR>(
567 auth: &RequestAuth,
568 recrypt: &Recrypt<Sha256, Ed25519, RandomBytes<CR>>,
569 device_private_key: &PrivateKey,
570 source_path: &str,
571 destination_path: &str,
572 encrypted_deks: &[u8],
573) -> Result<DocumentFileDecryptUnmanagedResult>
574where
575 CR: CryptoRng,
576{
577 let (doc_header, source_file) = read_document_header(source_path)?;
578
579 let proto_edeks =
581 EncryptedDeksP::parse_from_bytes(encrypted_deks).map_err(IronOxideErr::from)?;
582 document_api::edeks_and_header_match_or_err(&proto_edeks, &doc_header)?;
583
584 let transform_resp = requests::edek_transform::edek_transform(auth, encrypted_deks).await?;
586 let requests::edek_transform::EdekTransformResponse {
587 user_or_group,
588 encrypted_symmetric_key,
589 } = transform_resp;
590
591 let sym_key = transform::decrypt_as_symmetric_key(
593 recrypt,
594 encrypted_symmetric_key.try_into()?,
595 device_private_key.recrypt_key(),
596 )?;
597
598 let key_bytes: [u8; AES_KEY_LEN] = *sym_key.bytes();
599 stream_decrypt_to_file(&key_bytes, &source_file, destination_path)?;
600
601 Ok(DocumentFileDecryptUnmanagedResult {
602 id: doc_header.document_id,
603 access_via: user_or_group,
604 })
605}
606
607#[cfg(test)]
608mod tests {
609 use super::*;
610 use std::fs;
611 use tempfile::NamedTempFile;
612
613 #[test]
614 fn cleanup_on_drop_deletes_uncommitted_file() {
615 let temp_file = NamedTempFile::new().expect("Failed to create temp file");
616 let path = temp_file.path().to_str().unwrap().to_string();
617
618 fs::write(&path, b"test content").expect("Failed to write");
620 assert!(fs::metadata(&path).is_ok(), "File should exist before drop");
621
622 let guard = CleanupOnDrop::new(&path);
624 drop(guard);
625
626 assert!(
628 fs::metadata(&path).is_err(),
629 "File should be deleted after drop without commit"
630 );
631 }
632
633 #[test]
634 fn cleanup_on_drop_preserves_committed_file() {
635 let temp_file = NamedTempFile::new().expect("Failed to create temp file");
636 let path = temp_file.path().to_str().unwrap().to_string();
637
638 fs::write(&path, b"test content").expect("Failed to write");
640 assert!(fs::metadata(&path).is_ok(), "File should exist before drop");
641
642 let guard = CleanupOnDrop::new(&path);
644 guard.commit();
645
646 assert!(
648 fs::metadata(&path).is_ok(),
649 "File should exist after committed drop"
650 );
651
652 let _ = fs::remove_file(&path);
654 }
655
656 #[test]
657 fn cleanup_on_drop_handles_already_deleted_file() {
658 let temp_file = NamedTempFile::new().expect("Failed to create temp file");
659 let path = temp_file.path().to_str().unwrap().to_string();
660
661 let _ = fs::remove_file(&path);
663
664 let guard = CleanupOnDrop::new(&path);
666 drop(guard); }
668
669 #[cfg(unix)]
670 mod unix_permissions {
671 use super::*;
672 use std::os::unix::fs::PermissionsExt;
673
674 #[test]
675 fn create_output_file_sets_restrictive_permissions() {
676 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
677 let path = temp_dir.path().join("test_output.txt");
678 let path_str = path.to_str().unwrap();
679
680 let file = create_output_file(path_str).expect("Failed to create file");
681 drop(file);
682
683 let metadata = fs::metadata(path_str).expect("Failed to get metadata");
684 let mode = metadata.permissions().mode() & 0o777;
685
686 assert_eq!(
687 mode, 0o600,
688 "File should have mode 0600 (owner read/write only)"
689 );
690
691 let _ = fs::remove_file(path_str);
693 }
694
695 #[test]
696 fn reset_file_permissions_sets_normal_permissions() {
697 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
698 let path = temp_dir.path().join("test_reset.txt");
699 let path_str = path.to_str().unwrap();
700
701 let file = create_output_file(path_str).expect("Failed to create file");
703 drop(file);
704
705 let metadata = fs::metadata(path_str).expect("Failed to get metadata");
707 let mode_before = metadata.permissions().mode() & 0o777;
708 assert_eq!(mode_before, 0o600);
709
710 reset_file_permissions(path_str).expect("Failed to reset permissions");
712
713 let metadata = fs::metadata(path_str).expect("Failed to get metadata");
715 let mode_after = metadata.permissions().mode() & 0o777;
716 assert_eq!(mode_after, 0o644, "File should have mode 0644 after reset");
717
718 let _ = fs::remove_file(path_str);
720 }
721 }
722
723 #[cfg(windows)]
724 mod windows_permissions {
725 use super::*;
726
727 #[test]
728 fn create_output_file_has_exclusive_access_on_windows() {
729 use std::io::Write;
730
731 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
732 let path = temp_dir.path().join("test_output.txt");
733 let path_str = path.to_str().unwrap();
734
735 let mut file = create_output_file(path_str).expect("Failed to create file");
738
739 file.write_all(b"test").expect("Failed to write");
741
742 let open_attempt = fs::File::open(path_str);
745 assert!(
746 open_attempt.is_err(),
747 "Opening file should fail while held with exclusive share_mode(0)"
748 );
749
750 drop(file);
752
753 let content = fs::read(path_str).expect("Failed to read file after handle released");
755 assert_eq!(content, b"test");
756
757 let _ = fs::remove_file(path_str);
759 }
760
761 #[test]
762 fn reset_file_permissions_is_noop_on_windows() {
763 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
764 let path = temp_dir.path().join("test_reset.txt");
765 let path_str = path.to_str().unwrap();
766
767 fs::write(path_str, b"test content").expect("Failed to write file");
769
770 reset_file_permissions(path_str).expect("reset_file_permissions should succeed");
772
773 let content = fs::read(path_str).expect("Failed to read file");
775 assert_eq!(content, b"test content");
776
777 let _ = fs::remove_file(path_str);
779 }
780 }
781}