| // Copyright 2023 Google LLC |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| #![allow( |
| clippy::unwrap_used, |
| clippy::expect_used, |
| clippy::indexing_slicing, |
| clippy::panic, |
| missing_docs |
| )] |
| |
| use crypto_provider::{CryptoProvider, CryptoRng}; |
| use crypto_provider_default::CryptoProviderImpl; |
| use np_adv::credential::matched::{ |
| EmptyMatchedCredential, MetadataMatchedCredential, WithMatchedCredential, |
| }; |
| use np_adv::extended::deserialize::{Section, V1DeserializedSection}; |
| use np_adv::extended::{V1IdentityToken, V1_ENCODING_UNENCRYPTED}; |
| use np_adv::{ |
| credential::{ |
| book::CredentialBookBuilder, |
| v1::{V1BroadcastCredential, V1DiscoveryCredential, V1}, |
| MatchableCredential, |
| }, |
| deserialization_arena, deserialize_advertisement, |
| extended::{ |
| data_elements::TxPowerDataElement, |
| de_type::HasDEType, |
| deserialize::VerificationMode, |
| serialize::{AdvBuilder, MicEncryptedSectionEncoder, UnencryptedSectionEncoder}, |
| NP_V1_ADV_MAX_SECTION_COUNT, |
| }, |
| shared_data::TxPower, |
| AdvDeserializationError, AdvDeserializationErrorDetailsHazmat, |
| }; |
| use np_hkdf::DerivedSectionKeys; |
| use serde::{Deserialize, Serialize}; |
| |
| #[test] |
| fn v1_deser_plaintext() { |
| let mut adv_builder = AdvBuilder::new(); |
| let mut section_builder = adv_builder.section_builder(UnencryptedSectionEncoder).unwrap(); |
| section_builder.add_de(&TxPowerDataElement::from(TxPower::try_from(6).unwrap())).unwrap(); |
| section_builder.add_to_advertisement::<CryptoProviderImpl>(); |
| let adv = adv_builder.into_advertisement(); |
| |
| let cred_book = CredentialBookBuilder::<EmptyMatchedCredential>::build_cached_slice_book::< |
| 0, |
| 0, |
| CryptoProviderImpl, |
| >(&[], &[]); |
| |
| let arena = deserialization_arena!(); |
| let contents = |
| deserialize_advertisement::<_, CryptoProviderImpl>(arena, adv.as_slice(), &cred_book) |
| .expect("Should be a valid advertisemement") |
| .into_v1() |
| .expect("Should be V1"); |
| |
| assert_eq!(0, contents.invalid_sections_count()); |
| |
| let sections = contents.sections().collect::<Vec<_>>(); |
| |
| assert_eq!(1, sections.len()); |
| |
| let section = match §ions[0] { |
| V1DeserializedSection::Plaintext(s) => s, |
| _ => panic!("this is a plaintext adv"), |
| }; |
| let data_elements = section.iter_data_elements().collect::<Result<Vec<_>, _>>().unwrap(); |
| assert_eq!(1, data_elements.len()); |
| |
| let de = &data_elements[0]; |
| assert_eq!(None, de.salt()); |
| assert_eq!(TxPowerDataElement::DE_TYPE, de.de_type()); |
| assert_eq!(&[6], de.contents()); |
| } |
| |
| /// Sample contents for some encrypted identity metadata |
| /// which consists of a UUID together with a display name |
| /// and a general location. |
| #[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] |
| struct IdentityMetadata { |
| uuid: String, |
| display_name: String, |
| location: String, |
| } |
| |
| impl IdentityMetadata { |
| /// Serialize this identity metadata to a json byte-string. |
| fn to_bytes(&self) -> Vec<u8> { |
| serde_json::to_vec(self).expect("Identity metadata serialization is infallible") |
| } |
| /// Attempt to deserialize identity metadata from a json byte-string. |
| fn try_from_bytes(serialized: &[u8]) -> Option<Self> { |
| serde_json::from_slice(serialized).ok() |
| } |
| } |
| |
| #[test] |
| fn v1_deser_ciphertext() { |
| // identity material |
| let mut rng = <CryptoProviderImpl as CryptoProvider>::CryptoRng::new(); |
| let token_array: [u8; 16] = rng.gen(); |
| let identity_token = V1IdentityToken::from(token_array); |
| let key_seed = rng.gen(); |
| let hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(&key_seed); |
| |
| let broadcast_cred = V1BroadcastCredential::new(key_seed, identity_token); |
| |
| // Serialize and encrypt some identity metadata (sender-side) |
| let sender_metadata = IdentityMetadata { |
| uuid: "378845e1-2616-420d-86f5-674177a7504d".to_string(), |
| display_name: "Alice".to_string(), |
| location: "Wonderland".to_string(), |
| }; |
| let sender_metadata_bytes = sender_metadata.to_bytes(); |
| let encrypted_sender_metadata = MetadataMatchedCredential::<Vec<u8>>::encrypt_from_plaintext::< |
| V1, |
| CryptoProviderImpl, |
| >(&hkdf, identity_token, &sender_metadata_bytes); |
| |
| // prepare advertisement |
| let mut adv_builder = AdvBuilder::new(); |
| |
| let mut section_builder = adv_builder |
| .section_builder(MicEncryptedSectionEncoder::new_random_salt::<CryptoProviderImpl>( |
| &mut rng, |
| &broadcast_cred, |
| )) |
| .unwrap(); |
| section_builder.add_de(&TxPowerDataElement::from(TxPower::try_from(7).unwrap())).unwrap(); |
| section_builder.add_to_advertisement::<CryptoProviderImpl>(); |
| let adv = adv_builder.into_advertisement(); |
| println!("adv: {adv:?}"); |
| |
| let discovery_credential = V1DiscoveryCredential::new( |
| key_seed, |
| hkdf.v1_mic_short_salt_keys() |
| .identity_token_hmac_key() |
| .calculate_hmac::<CryptoProviderImpl>(identity_token.bytes()), |
| hkdf.v1_mic_extended_salt_keys() |
| .identity_token_hmac_key() |
| .calculate_hmac::<CryptoProviderImpl>(identity_token.bytes()), |
| ); |
| |
| let credentials: [MatchableCredential<V1, MetadataMatchedCredential<_>>; 1] = |
| [MatchableCredential { |
| discovery_credential, |
| match_data: encrypted_sender_metadata.clone(), |
| }]; |
| let cred_book = CredentialBookBuilder::build_cached_slice_book::<0, 0, CryptoProviderImpl>( |
| &[], |
| &credentials, |
| ); |
| let arena = deserialization_arena!(); |
| let contents = |
| deserialize_advertisement::<_, CryptoProviderImpl>(arena, adv.as_slice(), &cred_book) |
| .expect("Should be a valid advertisement") |
| .into_v1() |
| .expect("Should be V1"); |
| |
| assert_eq!(0, contents.invalid_sections_count(), "{contents:?}"); |
| |
| let sections = contents.sections().collect::<Vec<_>>(); |
| assert_eq!(1, sections.len()); |
| |
| let matched: &WithMatchedCredential<_, _> = match §ions[0] { |
| V1DeserializedSection::Decrypted(d) => d, |
| _ => panic!("this is a ciphertext adv"), |
| }; |
| |
| let decrypted_metadata_bytes = matched |
| .decrypt_metadata::<CryptoProviderImpl>() |
| .expect("Sender metadata should be decryptable"); |
| let decrypted_metadata = IdentityMetadata::try_from_bytes(&decrypted_metadata_bytes) |
| .expect("Sender metadata should be deserializable"); |
| assert_eq!(sender_metadata, decrypted_metadata); |
| |
| let section = matched.contents(); |
| |
| assert_eq!(VerificationMode::Mic, section.verification_mode()); |
| assert_eq!(&identity_token, section.identity_token()); |
| |
| let data_elements = section.iter_data_elements().collect::<Result<Vec<_>, _>>().unwrap(); |
| assert_eq!(1, data_elements.len()); |
| |
| let de = &data_elements[0]; |
| assert_eq!(TxPowerDataElement::DE_TYPE, de.de_type()); |
| assert_eq!(&[7], de.contents()); |
| } |
| |
| #[test] |
| fn v1_deser_no_section() { |
| // TODO: we shouldn't allow this invalid advertisement to be serialized |
| let adv_builder = AdvBuilder::new(); |
| let adv = adv_builder.into_advertisement(); |
| let cred_book = CredentialBookBuilder::<EmptyMatchedCredential>::build_cached_slice_book::< |
| 0, |
| 0, |
| CryptoProviderImpl, |
| >(&[], &[]); |
| let arena = deserialization_arena!(); |
| let v1_deserialize_error = |
| deserialize_advertisement::<_, CryptoProviderImpl>(arena, adv.as_slice(), &cred_book) |
| .expect_err(" Expected an error"); |
| assert_eq!( |
| v1_deserialize_error, |
| AdvDeserializationError::ParseError { |
| details_hazmat: AdvDeserializationErrorDetailsHazmat::AdvertisementDeserializeError |
| } |
| ); |
| } |
| |
| #[test] |
| fn v1_deser_plaintext_over_max_sections() { |
| let mut adv_builder = AdvBuilder::new(); |
| for _ in 0..NP_V1_ADV_MAX_SECTION_COUNT { |
| let mut section_builder = adv_builder.section_builder(UnencryptedSectionEncoder).unwrap(); |
| section_builder.add_de(&TxPowerDataElement::from(TxPower::try_from(7).unwrap())).unwrap(); |
| section_builder.add_to_advertisement::<CryptoProviderImpl>(); |
| } |
| let mut adv = adv_builder.into_advertisement().as_slice().to_vec(); |
| // Push an extra section |
| adv.extend_from_slice( |
| [ |
| 0x01, // Section header |
| V1_ENCODING_UNENCRYPTED.byte_value(), |
| ] |
| .as_slice(), |
| ); |
| let cred_book = CredentialBookBuilder::<EmptyMatchedCredential>::build_cached_slice_book::< |
| 0, |
| 0, |
| CryptoProviderImpl, |
| >(&[], &[]); |
| let arena = deserialization_arena!(); |
| assert_eq!( |
| deserialize_advertisement::<_, CryptoProviderImpl>(arena, adv.as_slice(), &cred_book) |
| .unwrap_err(), |
| AdvDeserializationError::ParseError { |
| details_hazmat: AdvDeserializationErrorDetailsHazmat::AdvertisementDeserializeError |
| } |
| ); |
| } |