Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 4.3.0

- [[#373](https://github.com/IronCoreLabs/ironoxide/pull/373)] Add managed and unmanaged file encrypt/decrypt, which internally stream the bytes for constant memory usage. Note the security disclaimer on file decrypt if you intend to use these APIs.

## 4.2.1

- [[#372](https://github.com/IronCoreLabs/ironoxide/pull/372)] Hotfix serialized public key cache so that offline initialization works. This fix does require cache regeneration.
Expand Down
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ironoxide"
version = "4.2.1"
version = "4.3.0"
authors = ["IronCore Labs <info@ironcorelabs.com>"]
readme = "README.md"
license = "AGPL-3.0-only"
Expand Down Expand Up @@ -35,6 +35,7 @@ base64 = "0.22"
base64-serde = "0.8"
bytes = "1"
futures = "0.3.1"
ghash = "0.5" # Incremental GHASH computation for streaming AES-GCM
hex = "0.4"
ironcore-search-helpers = { version = "0.2", optional = true }
itertools = "0.14"
Expand Down Expand Up @@ -68,6 +69,8 @@ criterion = "0.8"
double = "0.2.4"
galvanic-assert = "0.8"
mut_static = "5"
proptest = "1.6"
tempfile = "3"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
uuid = { version = "1.0", features = ["v4"], default-features = false }

Expand Down
33 changes: 18 additions & 15 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,22 @@
flake-utils.url = "github:numtide/flake-utils";
};

outputs = { self, nixpkgs, rust-overlay, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (system:
let
overlays = [ (import rust-overlay) ];
pkgs = import nixpkgs { inherit system overlays; };
rusttoolchain =
pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
in rec {
# nix develop
devShell = pkgs.mkShell {
buildInputs = with pkgs;
[ rusttoolchain pkg-config openssl ];
};

});
outputs = {
self,
nixpkgs,
rust-overlay,
flake-utils,
...
}:
flake-utils.lib.eachDefaultSystem (system: let
overlays = [(import rust-overlay)];
pkgs = import nixpkgs {inherit system overlays;};
rusttoolchain =
pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
in rec {
# nix develop
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [rusttoolchain pkg-config openssl];
};
});
}
57 changes: 57 additions & 0 deletions src/blocking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,63 @@ impl BlockingIronOxide {
self.ironoxide
.document_revoke_access_unmanaged(edeks, revoke_list)
}

/// See [ironoxide::document::file::DocumentFileOps::document_file_encrypt](trait.DocumentFileOps.html#tymethod.document_file_encrypt)
pub fn document_file_encrypt(
&self,
source_path: &str,
destination_path: &str,
opts: &DocumentEncryptOpts,
) -> Result<DocumentFileEncryptResult> {
self.runtime.block_on(self.ironoxide.document_file_encrypt(
source_path,
destination_path,
opts,
))
}

/// See [ironoxide::document::file::DocumentFileOps::document_file_decrypt](trait.DocumentFileOps.html#tymethod.document_file_decrypt)
pub fn document_file_decrypt(
&self,
source_path: &str,
destination_path: &str,
) -> Result<DocumentFileDecryptResult> {
self.runtime.block_on(
self.ironoxide
.document_file_decrypt(source_path, destination_path),
)
}

/// See [ironoxide::document::file::DocumentFileAdvancedOps::document_file_encrypt_unmanaged](trait.DocumentFileAdvancedOps.html#tymethod.document_file_encrypt_unmanaged)
pub fn document_file_encrypt_unmanaged(
&self,
source_path: &str,
destination_path: &str,
opts: &DocumentEncryptOpts,
) -> Result<DocumentFileEncryptUnmanagedResult> {
self.runtime
.block_on(self.ironoxide.document_file_encrypt_unmanaged(
source_path,
destination_path,
opts,
))
}

/// See [ironoxide::document::file::DocumentFileAdvancedOps::document_file_decrypt_unmanaged](trait.DocumentFileAdvancedOps.html#tymethod.document_file_decrypt_unmanaged)
pub fn document_file_decrypt_unmanaged(
&self,
source_path: &str,
destination_path: &str,
encrypted_deks: &[u8],
) -> Result<DocumentFileDecryptUnmanagedResult> {
self.runtime
.block_on(self.ironoxide.document_file_decrypt_unmanaged(
source_path,
destination_path,
encrypted_deks,
))
}

/// See [ironoxide::IronOxide::export_public_key_cache](../struct.IronOxide.html#method.export_public_key_cache)
pub fn export_public_key_cache(&self) -> Result<Vec<u8>> {
self.ironoxide.export_public_key_cache()
Expand Down
76 changes: 61 additions & 15 deletions src/crypto/aes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ use std::{convert::TryFrom, ops::DerefMut, sync::Mutex};
//There is no way this can fail. Value is most definitely not less than one.
const PBKDF2_ITERATIONS: NonZeroU32 = NonZeroU32::new(250_000).unwrap();
const PBKDF2_SALT_LEN: usize = 32;
const AES_GCM_TAG_LEN: usize = 16;
const AES_IV_LEN: usize = 12;
const AES_KEY_LEN: usize = 32;
pub(crate) const AES_GCM_TAG_LEN: usize = 16;
pub(crate) const AES_IV_LEN: usize = 12;
pub(crate) const AES_KEY_LEN: usize = 32;
/// Byte size of AES block (128, 192, and 256 bit keys all have 128 bit blocks)
pub(crate) const AES_BLOCK_SIZE: usize = 16; // 128 bit / 8 bits per byte
//The encrypted user master key length will be the size of the encrypted key (32 bytes) plus the size of the GCM auth tag (16 bytes).
const ENCRYPTED_KEY_AND_GCM_TAG_LEN: usize = AES_KEY_LEN + AES_GCM_TAG_LEN;

Expand Down Expand Up @@ -245,6 +247,8 @@ pub fn decrypt(
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::streaming::tests::{generate_test_key, test_rng};
use proptest::prelude::*;
use std::{convert::TryInto, sync::Arc};

#[test]
Expand Down Expand Up @@ -275,11 +279,9 @@ mod tests {
#[test]
fn test_encrypt() {
let plaintext = vec![1, 2, 3, 4, 5, 6, 7];
let mut key = [0u8; 32];
let mut rng = rand::thread_rng();
rng.fill_bytes(&mut key);

let res = encrypt(&Mutex::new(rng), plaintext.clone(), key).unwrap();
let key = generate_test_key();
let rng = test_rng();
let res = encrypt(&rng, plaintext.clone(), key).unwrap();
assert_eq!(res.aes_iv.len(), 12);
assert_eq!(
res.ciphertext.len(),
Expand All @@ -290,11 +292,10 @@ mod tests {
#[test]
fn test_decrypt() {
let plaintext = vec![1, 2, 3, 4, 5, 6, 7];
let mut key = [0u8; 32];
let mut rng = rand::thread_rng();
rng.fill_bytes(&mut key);
let key = generate_test_key();
let rng = test_rng();

let mut encrypted_result = encrypt(&Mutex::new(rng), plaintext.clone(), key).unwrap();
let mut encrypted_result = encrypt(&rng, plaintext.clone(), key).unwrap();

let decrypted_plaintext = decrypt(&mut encrypted_result, key).unwrap();

Expand All @@ -321,11 +322,9 @@ mod tests {

#[test]
fn test_parallel_encrypt() {
use rand::SeedableRng;

let plaintext = vec![1, 2, 3, 4, 5, 6, 7];
let mut key = [0u8; 32];
let rng = Mutex::new(rand_chacha::ChaChaRng::from_entropy());
let rng = test_rng();
take_lock(&rng).deref_mut().fill_bytes(&mut key);

let a_rng = Arc::new(rng);
Expand All @@ -347,4 +346,51 @@ mod tests {

assert_eq!(joined_count, 100);
}

// pulled from https://docs.rs/proptest/latest/src/proptest/array.rs.html#213
fn uniform48<S: Strategy>(
strategy: S,
) -> proptest::array::UniformArrayStrategy<S, [S::Value; 48]> {
proptest::array::UniformArrayStrategy::new(strategy)
}
proptest! {
#[test]
fn prop_encrypt_decrypt_roundtrip(plaintext in prop::collection::vec(any::<u8>(), 0..10000)) {
let key = generate_test_key();
let rng = test_rng();

let mut encrypted = encrypt(&rng, plaintext.clone(), key).unwrap();
let decrypted = decrypt(&mut encrypted, key).unwrap();

prop_assert_eq!(&plaintext[..], decrypted);
}

#[test]
fn prop_aes_encrypted_value_roundtrip_bytes(
iv in prop::array::uniform12(any::<u8>()),
ciphertext in prop::collection::vec(any::<u8>(), AES_GCM_TAG_LEN..1000)
) {
let value = AesEncryptedValue { aes_iv: iv, ciphertext };
let bytes = value.bytes();
let restored: AesEncryptedValue = bytes.as_slice().try_into().unwrap();

prop_assert_eq!(value.aes_iv, restored.aes_iv);
prop_assert_eq!(value.ciphertext, restored.ciphertext);
}

#[test]
fn prop_encrypted_master_key_roundtrip_bytes(
salt in prop::array::uniform32(any::<u8>()),
iv in prop::array::uniform12(any::<u8>()),
encrypted_key in uniform48(any::<u8>())
) {
let key = EncryptedMasterKey::new(salt, iv, encrypted_key);
let bytes = key.bytes();
let restored = EncryptedMasterKey::new_from_slice(&bytes).unwrap();

prop_assert_eq!(key.pbkdf2_salt, restored.pbkdf2_salt);
prop_assert_eq!(key.aes_iv, restored.aes_iv);
prop_assert_eq!(key.encrypted_key, restored.encrypted_key);
}
}
}
Loading
Loading