diff --git a/Cargo.toml b/Cargo.toml index 1c7c198..d308059 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,11 +4,15 @@ members = [ "crates/*" ] [workspace.dependencies] age = "0.9" +aes-gcm = "0.10" +base64 = "0.21" +derive_more = "0.99" indoc = "2" rand = "0.8" serde = { version = "1", features = ["derive"] } serde_with = "3" serde_yaml = "0.9" +strum = { version = "0.25.0", features = ["derive"] } textwrap = "0.16" thiserror = "1" diff --git a/README.md b/README.md index eb7faa8..870ddcd 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ - Flag: `--mac-only-encrypted`. - `.rops.yaml`: `partial_encryption.mac_only_encrypted: true`. - [ ] Last modified metadata +- [ ] File comment encryption ### Integrations: diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index 10851b8..ff9daea 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -9,13 +9,18 @@ age = ["dep:age"] age-test-utils = ["age", "test-utils", "dep:indoc", "dep:textwrap"] # File formats: yaml = ["dep:serde_yaml"] +# Ciphers +aes-gcm = ["dep:aes-gcm"] test-utils = [] [dependencies] +base64.workspace = true +derive_more.workspace = true rand.workspace = true serde.workspace = true serde_with.workspace = true +strum.workspace = true thiserror.workspace = true # AGE @@ -27,3 +32,6 @@ indoc = { workspace = true, optional = true } # YAML serde_yaml = { workspace = true, optional = true } + +# AES_GCM +aes-gcm = { workspace = true, optional = true } diff --git a/crates/lib/src/base64_utils.rs b/crates/lib/src/base64_utils.rs new file mode 100644 index 0000000..91dba52 --- /dev/null +++ b/crates/lib/src/base64_utils.rs @@ -0,0 +1,16 @@ +use base64::{engine::general_purpose, Engine}; + +pub trait Base64Utils +where + Self: AsRef<[u8]>, +{ + fn encode_base64(&self) -> String { + general_purpose::STANDARD.encode(self.as_ref()) + } + + fn decode_base64(&self) -> Result, base64::DecodeError> { + general_purpose::STANDARD.decode(self.as_ref()) + } +} + +impl> Base64Utils for T {} diff --git a/crates/lib/src/cipher/aes256_gcm.rs b/crates/lib/src/cipher/aes256_gcm.rs new file mode 100644 index 0000000..4336c72 --- /dev/null +++ b/crates/lib/src/cipher/aes256_gcm.rs @@ -0,0 +1,9 @@ +use crate::*; + +pub struct AES256GCM; + +impl Cipher for AES256GCM { + fn authorization_tag_size(&self) -> usize { + 16 + } +} diff --git a/crates/lib/src/cipher/core.rs b/crates/lib/src/cipher/core.rs new file mode 100644 index 0000000..66f45fb --- /dev/null +++ b/crates/lib/src/cipher/core.rs @@ -0,0 +1,3 @@ +pub trait Cipher { + fn authorization_tag_size(&self) -> usize; +} diff --git a/crates/lib/src/cipher/mod.rs b/crates/lib/src/cipher/mod.rs new file mode 100644 index 0000000..edcf752 --- /dev/null +++ b/crates/lib/src/cipher/mod.rs @@ -0,0 +1,10 @@ +mod core; +pub use core::Cipher; + +mod variant; +pub use variant::CipherVariant; + +#[cfg(feature = "aes-gcm")] +mod aes256_gcm; +#[cfg(feature = "aes-gcm")] +pub use aes256_gcm::AES256GCM; diff --git a/crates/lib/src/cipher/variant.rs b/crates/lib/src/cipher/variant.rs new file mode 100644 index 0000000..d496aa3 --- /dev/null +++ b/crates/lib/src/cipher/variant.rs @@ -0,0 +1,37 @@ +use strum::{AsRefStr, EnumString}; + +use crate::*; + +#[derive(Debug, PartialEq, AsRefStr, EnumString)] +pub enum CipherVariant { + #[cfg(feature = "aes-gcm")] + #[strum(serialize = "AES256_GCM")] + AES256GCM, +} + +impl CipherVariant { + pub fn cipher(&self) -> &dyn Cipher { + match self { + #[cfg(feature = "aes-gcm")] + CipherVariant::AES256GCM => &AES256GCM, + } + } +} + +#[cfg(test)] +mod tests { + #[cfg(feature = "aes-gcm")] + mod aes_gcm { + use crate::*; + + #[test] + fn displays_aes256_gcm_cipher() { + assert_eq!("AES256_GCM", CipherVariant::AES256GCM.as_ref()) + } + + #[test] + fn parses_aes256_gcm_cipher() { + assert_eq!(CipherVariant::AES256GCM, "AES256_GCM".parse::().unwrap()) + } + } +} diff --git a/crates/lib/src/data_key.rs b/crates/lib/src/data_key.rs index 4cbd21e..a104359 100644 --- a/crates/lib/src/data_key.rs +++ b/crates/lib/src/data_key.rs @@ -1,53 +1,43 @@ -use rand::RngCore; +use derive_more::{AsMut, AsRef}; -pub const DATA_KEY_BYTE_SIZE: usize = 32; +use crate::*; + +const DATA_KEY_SIZE: usize = 32; // FIXME: zeroize upon drop? -#[derive(Debug, PartialEq)] -pub struct DataKey([u8; DATA_KEY_BYTE_SIZE]); +#[derive(Debug, PartialEq, AsRef, AsMut)] +#[as_ref(forward)] +#[as_mut(forward)] +pub struct DataKey(RngKey); impl DataKey { #[allow(clippy::new_without_default)] pub fn new() -> Self { - // Assumed to be cryptographically secure. Uses ChaCha12 as - // the PRNG with OS provided RNG (e.g getrandom) for the - // initial seed. - // - // https://docs.rs/rand/latest/rand/rngs/struct.ThreadRng.html - let mut rand = rand::thread_rng(); - - let mut inner = [0u8; DATA_KEY_BYTE_SIZE]; - rand.fill_bytes(&mut inner); - Self(inner) - } - - pub fn empty() -> Self { - Self([0; DATA_KEY_BYTE_SIZE]) + Self(RngKey::new()) } -} -impl AsRef<[u8]> for DataKey { - fn as_ref(&self) -> &[u8] { - self.0.as_slice() + pub const fn empty() -> Self { + Self(RngKey::empty()) } -} -impl AsMut<[u8]> for DataKey { - fn as_mut(&mut self) -> &mut [u8] { - self.0.as_mut_slice() + pub const fn byte_size() -> usize { + RngKey::<{ DATA_KEY_SIZE }>::byte_size() } } #[cfg(feature = "test-utils")] -mod test_utils { +mod mock { use crate::*; impl MockTestUtil for DataKey { fn mock() -> Self { - Self([ - 67, 11, 25, 39, 242, 246, 79, 131, 60, 80, 226, 83, 115, 116, 50, 131, 39, 148, 220, 226, 136, 158, 165, 19, 155, 218, 16, - 53, 47, 24, 192, 26, - ]) + Self( + [ + 254, 79, 93, 103, 195, 165, 169, 238, 35, 187, 236, 95, 222, 243, 40, 26, 130, 128, 59, 176, 15, 195, 55, 93, 129, 212, + 57, 80, 15, 181, 72, 114, + ] + .into(), + ) } } } @@ -58,16 +48,6 @@ mod tests { #[test] fn data_key_is_256_bits() { - assert_eq!(256, DATA_KEY_BYTE_SIZE * 8) - } - - #[test] - fn new_data_key_not_zeroed() { - assert_ne!([0; DATA_KEY_BYTE_SIZE], DataKey::new().as_ref()) - } - - #[test] - fn seemingly_random() { - assert_ne!(DataKey::new(), DataKey::new()) + assert_eq!(256, DATA_KEY_SIZE * 8) } } diff --git a/crates/lib/src/encrypted_value/data.rs b/crates/lib/src/encrypted_value/data.rs new file mode 100644 index 0000000..ed01352 --- /dev/null +++ b/crates/lib/src/encrypted_value/data.rs @@ -0,0 +1,56 @@ +use derive_more::AsRef; + +use crate::*; + +#[derive(Debug, PartialEq)] +pub struct EncryptedValueData(Vec); + +#[derive(AsRef)] +#[as_ref(forward)] +pub struct EncryptedValueDataAuthorizationTag<'a>(&'a [u8]); + +#[derive(AsRef)] +#[as_ref(forward)] +pub struct EncryptedValueDataExceptTag<'a>(&'a [u8]); + +impl EncryptedValueData { + pub fn tag(&self, cipher: &dyn Cipher) -> EncryptedValueDataAuthorizationTag { + EncryptedValueDataAuthorizationTag(&self.0[self.cipher_authorization_tag_start_index(cipher)..]) + } + + pub fn except_tag(&self, cipher: &dyn Cipher) -> EncryptedValueDataExceptTag { + EncryptedValueDataExceptTag(&self.0[..self.cipher_authorization_tag_start_index(cipher)]) + } + + fn cipher_authorization_tag_start_index(&self, cipher: &dyn Cipher) -> usize { + self.0 + .len() + .checked_sub(cipher.authorization_tag_size()) + .expect("minimum encrypted value length less than cipher authorization tag size") + } +} + +#[cfg(feature = "test-utils")] +mod mock { + use super::*; + + impl MockStringTestUtil for EncryptedValueDataExceptTag<'_> { + fn mock_string() -> String { + "3S1E9am/".to_string() + } + } + + impl MockStringTestUtil for EncryptedValueDataAuthorizationTag<'_> { + fn mock_string() -> String { + "nQUDkuh0OR1cjR5hGC5jOw==".to_string() + } + } + + impl MockTestUtil for EncryptedValueData { + fn mock() -> Self { + Self(vec![ + 221, 45, 68, 245, 169, 191, 157, 5, 3, 146, 232, 116, 57, 29, 92, 141, 30, 97, 24, 46, 99, 59, + ]) + } + } +} diff --git a/crates/lib/src/encrypted_value/metadata.rs b/crates/lib/src/encrypted_value/metadata.rs new file mode 100644 index 0000000..44ea215 --- /dev/null +++ b/crates/lib/src/encrypted_value/metadata.rs @@ -0,0 +1,23 @@ +use crate::*; + +#[derive(Debug, PartialEq)] +pub struct EncryptedValueMetaData { + pub cipher_variant: CipherVariant, + pub initial_value: InitialValue, + pub value_type: ValueType, +} + +#[cfg(feature = "test-utils")] +mod mock { + use super::*; + + impl MockTestUtil for EncryptedValueMetaData { + fn mock() -> Self { + Self { + cipher_variant: CipherVariant::AES256GCM, + initial_value: MockTestUtil::mock(), + value_type: ValueType::String, + } + } + } +} diff --git a/crates/lib/src/encrypted_value/mod.rs b/crates/lib/src/encrypted_value/mod.rs new file mode 100644 index 0000000..68954c2 --- /dev/null +++ b/crates/lib/src/encrypted_value/mod.rs @@ -0,0 +1,8 @@ +mod data; +pub use data::{EncryptedValueData, EncryptedValueDataAuthorizationTag, EncryptedValueDataExceptTag}; + +mod metadata; +pub use metadata::EncryptedValueMetaData; + +mod value; +pub use value::EncryptedValue; diff --git a/crates/lib/src/encrypted_value/value.rs b/crates/lib/src/encrypted_value/value.rs new file mode 100644 index 0000000..3eeab87 --- /dev/null +++ b/crates/lib/src/encrypted_value/value.rs @@ -0,0 +1,79 @@ +// GOAL: serialize age into +// ENC[AES256_GCM,data:EjRPNlhx,iv:XmS4b2ZqB39Qjpl/IQRm36KLclV8wXuBjuZsw4yekcU=,tag: +// SWK3XZBBUA49muEyeqld4g==,type:str] + +use std::fmt::{Display, Formatter}; + +use crate::*; + +#[derive(Debug, PartialEq)] +pub struct EncryptedValue { + data: EncryptedValueData, + metadata: EncryptedValueMetaData, +} + +impl Display for EncryptedValue { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "ENC[{},data:{},iv:{},tag:{},type:{}]", + self.metadata.cipher_variant.as_ref(), + self.data.except_tag(self.metadata.cipher_variant.cipher()).encode_base64(), + self.metadata.initial_value.encode_base64(), + self.data.tag(self.metadata.cipher_variant.cipher()).encode_base64(), + self.metadata.value_type.as_ref(), + ) + } +} + +mod base64 { + use base64::{engine::general_purpose, Engine}; + + pub trait Base64 + where + Self: AsRef<[u8]>, + { + fn as_base64(&self) -> String { + general_purpose::STANDARD.encode(self.as_ref()) + } + } + + impl> Base64 for T {} +} + +#[cfg(feature = "test-utils")] +mod mock { + use super::*; + + impl MockTestUtil for EncryptedValue { + fn mock() -> Self { + Self { + data: MockTestUtil::mock(), + metadata: MockTestUtil::mock(), + } + } + } + + impl MockStringTestUtil for EncryptedValue { + fn mock_string() -> String { + format!( + "ENC[AES256_GCM,data:{},iv:kwtVOk4u/wLHMovHYG2ngLv+uM8U9UJrIxjS6zCKmVY=,tag:{},type:str]", + EncryptedValueDataExceptTag::mock_string(), + EncryptedValueDataAuthorizationTag::mock_string() + ) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn displays_value_encryption_content() { + DisplayTestUtils::assert_display::() + } + + #[test] + fn parses_value_encryption_content() {} +} diff --git a/crates/lib/src/initial_value.rs b/crates/lib/src/initial_value.rs new file mode 100644 index 0000000..fe2cbe7 --- /dev/null +++ b/crates/lib/src/initial_value.rs @@ -0,0 +1,51 @@ +use crate::*; + +const INITIAL_VALUE_SIZE: usize = 32; + +#[derive(Debug, PartialEq)] +pub struct InitialValue(RngKey); + +impl InitialValue { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self(RngKey::new()) + } + + pub const fn byte_size() -> usize { + RngKey::<{ INITIAL_VALUE_SIZE }>::byte_size() + } +} + +// TEMP(WORKAROUND): derive_more::AsRef doesn't seem to work +impl AsRef<[u8]> for InitialValue { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +#[cfg(feature = "test-utils")] +mod mock { + use super::*; + + impl MockTestUtil for InitialValue { + fn mock() -> Self { + Self( + [ + 147, 11, 85, 58, 78, 46, 255, 2, 199, 50, 139, 199, 96, 109, 167, 128, 187, 254, 184, 207, 20, 245, 66, 107, 35, 24, + 210, 235, 48, 138, 153, 86, + ] + .into(), + ) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn initial_value_is_256_bits() { + assert_eq!(256, INITIAL_VALUE_SIZE * 8) + } +} diff --git a/crates/lib/src/integration/age.rs b/crates/lib/src/integration/age.rs index 5ce060a..4c3b057 100644 --- a/crates/lib/src/integration/age.rs +++ b/crates/lib/src/integration/age.rs @@ -37,7 +37,7 @@ impl Integration for AgeIntegration { let encryptor = age::Encryptor::with_recipients(vec![Box::new(public_key.clone())]).expect("provided recipients should be non-empty"); - let mut unarmored_encypted_buffer = Vec::with_capacity(DATA_KEY_BYTE_SIZE); + let mut unarmored_encypted_buffer = Vec::with_capacity(DataKey::byte_size()); let mut encryption_writer = encryptor.wrap_output(&mut unarmored_encypted_buffer)?; encryption_writer.write_all(data_key.as_ref())?; encryption_writer.finish()?; @@ -71,7 +71,7 @@ impl Integration for AgeIntegration { } #[cfg(feature = "test-utils")] -mod test_utils { +mod mock { use super::*; impl IntegrationTestUtils for AgeIntegration { @@ -86,11 +86,11 @@ mod test_utils { fn mock_encrypted_data_key_str() -> &'static str { indoc::indoc! {" -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuTkhudTlUaFRKdWlwZFRs - cG1KQW1rQk51Z2tpYy85NDZOZDV6eUJlM0hJCkMwdGFZaWFCNjFFelhzMDg1U1dE - SU1WTU5aUVBUUGFYdjJtalpRNkFNejgKLS0tIFFkOXUwaWNHY1pWUTQxZGhtMWpR - UG93akdhZm43WHZ6U3ZEc3dsVUlGWTgK5ViwbodEIX9YdSiQbbofnPvGVsTVVwp5 - +6TH7xovNbthvqDyOBVYv8g0Q+EUNjdQ3J6K3uJAdLDOCFzPincGPA== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKeE9VRHJpNmc4Z1NFeDd6 + L3cybjRHblYvaFUxbk9JZDZ4RFdENGpiNmhZCnZCRXRNSlRZeno0SDlJWXdhT0xl + Y1BlMzcyYUdVWFJ6WEVMTlRRaDRGbFUKLS0tIGc0V3gzU043MzBUd01BVTVKTEwr + azRyUldHUXo0cTV2YlZWa2pwcWFweGcKQdFW597WOM0bYfycoA2A0JxjKlrka+lc + MLuTri7QMM+g8yXcjneEGxjobGIqnvARlzDwcnFMxBoZ5/KRjMipXA== -----END AGE ENCRYPTED FILE----- "} } diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index bf0d070..23e9809 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -1,27 +1,55 @@ -mod data_key; -pub use data_key::{DataKey, DATA_KEY_BYTE_SIZE}; - mod error_handling; pub use error_handling::{RopsError, RopsResult}; -mod sops_file; -pub use sops_file::*; +mod rops_file; +pub use rops_file::*; mod integration; pub use integration::*; +mod encrypted_value; +pub use encrypted_value::*; + +mod rng_key; +pub use rng_key::RngKey; + +mod data_key; +pub use data_key::DataKey; + +mod initial_value; +pub use initial_value::InitialValue; + +mod value; +pub use value::ValueType; + +mod cipher; +pub use cipher::*; + +mod base64_utils; +pub use base64_utils::Base64Utils; + +#[cfg(feature = "test-utils")] +mod mock; +#[cfg(feature = "test-utils")] +pub use mock::MockTestUtil; + #[cfg(feature = "test-utils")] -pub use mocking::MockTestUtil; -#[cfg(all(feature = "test-utils", feature = "yaml"))] -pub use mocking::MockYamlTestUtil; +pub use display_test_utils::{DisplayTestUtils, MockStringTestUtil}; #[cfg(feature = "test-utils")] -mod mocking { - pub trait MockTestUtil { - fn mock() -> Self; +mod display_test_utils { + use std::fmt::Display; + + use crate::*; + + pub trait MockStringTestUtil { + fn mock_string() -> String; } - #[cfg(feature = "yaml")] - pub trait MockYamlTestUtil { - fn mock_yaml() -> String; + pub struct DisplayTestUtils; + + impl DisplayTestUtils { + pub fn assert_display() { + assert_eq!(T::mock_string(), T::mock().to_string()) + } } } diff --git a/crates/lib/src/mock.rs b/crates/lib/src/mock.rs new file mode 100644 index 0000000..b7c87c5 --- /dev/null +++ b/crates/lib/src/mock.rs @@ -0,0 +1,3 @@ +pub trait MockTestUtil { + fn mock() -> Self; +} diff --git a/crates/lib/src/rng_key.rs b/crates/lib/src/rng_key.rs new file mode 100644 index 0000000..31c156c --- /dev/null +++ b/crates/lib/src/rng_key.rs @@ -0,0 +1,58 @@ +use derive_more::{AsMut, AsRef}; +use rand::RngCore; + +#[derive(Debug, PartialEq, AsRef, AsMut)] +pub struct RngKey([u8; BYTE_SIZE]); + +impl RngKey { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + // Assumed to be cryptographically secure. Uses ChaCha12 as + // the PRNG with OS provided RNG (e.g getrandom) for the + // initial seed. + // + // https://docs.rs/rand/latest/rand/rngs/struct.ThreadRng.html + let mut rand = rand::thread_rng(); + + let mut inner = [0u8; BYTE_SIZE]; + rand.fill_bytes(&mut inner); + Self(inner) + } + + pub const fn byte_size() -> usize { + BYTE_SIZE + } + + pub const fn empty() -> Self { + Self([0; BYTE_SIZE]) + } +} + +#[cfg(feature = "test-utils")] +mod mock { + use super::*; + + impl From<[u8; BYTESIZE]> for RngKey { + fn from(bytes: [u8; BYTESIZE]) -> Self { + Self(bytes) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const MOCK_BYTE_SIZE: usize = 32; + type MockRngKey = RngKey<{ MOCK_BYTE_SIZE }>; + + #[test] + fn new_rng_key_not_zeroed() { + assert_ne!(&[0; MOCK_BYTE_SIZE], MockRngKey::new().as_ref()) + } + + #[test] + fn new_rng_key_seems_random() { + assert_ne!(MockRngKey::new(), MockRngKey::new()) + } +} diff --git a/crates/lib/src/sops_file/mod.rs b/crates/lib/src/rops_file/mod.rs similarity index 73% rename from crates/lib/src/sops_file/mod.rs rename to crates/lib/src/rops_file/mod.rs index 4abcc33..12d2963 100644 --- a/crates/lib/src/sops_file/mod.rs +++ b/crates/lib/src/rops_file/mod.rs @@ -1,28 +1,35 @@ -mod file_format {} +pub use file_format::*; +mod file_format { -#[cfg(feature = "test-utils")] -pub use test_utils::*; -#[cfg(feature = "test-utils")] -mod test_utils { #[cfg(feature = "yaml")] - pub use yaml::YamlTestUtils; + pub use yaml::*; #[cfg(feature = "yaml")] mod yaml { - use std::fmt::Debug; - use serde::{de::DeserializeOwned, Serialize}; + #[cfg(feature = "test-utils")] + pub use test_utils::{MockYamlTestUtil, YamlTestUtils}; + #[cfg(feature = "test-utils")] + mod test_utils { + use std::fmt::Debug; - use crate::*; + use serde::{de::DeserializeOwned, Serialize}; - pub struct YamlTestUtils; + use crate::*; - impl YamlTestUtils { - pub fn assert_serialization() { - assert_eq!(T::mock_yaml(), serde_yaml::to_string(&T::mock()).unwrap()) + pub trait MockYamlTestUtil { + fn mock_yaml() -> String; } - pub fn assert_deserialization() { - assert_eq!(T::mock(), serde_yaml::from_str(&T::mock_yaml()).unwrap()) + pub struct YamlTestUtils; + + impl YamlTestUtils { + pub fn assert_serialization() { + assert_eq!(T::mock_yaml(), serde_yaml::to_string(&T::mock()).unwrap()) + } + + pub fn assert_deserialization() { + assert_eq!(T::mock(), serde_yaml::from_str(&T::mock_yaml()).unwrap()) + } } } } @@ -63,7 +70,7 @@ mod metadata { } #[cfg(feature = "test-utils")] - mod test_utils { + mod mock { use super::*; impl MockTestUtil for SopsFileAgeMetadata { diff --git a/crates/lib/src/value.rs b/crates/lib/src/value.rs new file mode 100644 index 0000000..409820d --- /dev/null +++ b/crates/lib/src/value.rs @@ -0,0 +1,23 @@ +use strum::{AsRefStr, EnumString}; + +#[derive(Debug, PartialEq, AsRefStr, EnumString)] +pub enum ValueType { + #[strum(serialize = "str")] + String, + // .. etc +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn displays_string_type() { + assert_eq!("str", ValueType::String.as_ref()) + } + + #[test] + fn parses_string_type() { + assert_eq!(ValueType::String, "str".parse::().unwrap()) + } +}