diff --git a/aries/agents/aries-vcx-agent/src/handlers/out_of_band.rs b/aries/agents/aries-vcx-agent/src/handlers/out_of_band.rs index 6d01745a55..9aa8558fe7 100644 --- a/aries/agents/aries-vcx-agent/src/handlers/out_of_band.rs +++ b/aries/agents/aries-vcx-agent/src/handlers/out_of_band.rs @@ -52,7 +52,7 @@ impl ServiceOutOfBand { GenericOutOfBand::Sender(sender.to_owned()), )?; - Ok(sender.to_aries_message()) + Ok(sender.invitation_to_aries_message()) } pub fn receive_invitation(&self, invitation: AriesMessage) -> AgentResult { diff --git a/aries/aries_vcx/src/errors/mapping_others.rs b/aries/aries_vcx/src/errors/mapping_others.rs index b58b243a86..8709c69450 100644 --- a/aries/aries_vcx/src/errors/mapping_others.rs +++ b/aries/aries_vcx/src/errors/mapping_others.rs @@ -1,7 +1,9 @@ -use std::{num::ParseIntError, sync::PoisonError}; +use std::{num::ParseIntError, string::FromUtf8Error, sync::PoisonError}; +use base64::DecodeError; use did_doc::schema::{types::uri::UriWrapperError, utils::error::DidDocumentLookupError}; use shared::errors::http_error::HttpError; +use url::ParseError; use crate::{ errors::error::{AriesVcxError, AriesVcxErrorKind}, @@ -92,3 +94,21 @@ impl From for AriesVcxError { AriesVcxError::from_msg(AriesVcxErrorKind::InvalidInput, err.to_string()) } } + +impl From for AriesVcxError { + fn from(err: DecodeError) -> Self { + AriesVcxError::from_msg(AriesVcxErrorKind::InvalidInput, err.to_string()) + } +} + +impl From for AriesVcxError { + fn from(err: FromUtf8Error) -> Self { + AriesVcxError::from_msg(AriesVcxErrorKind::InvalidInput, err.to_string()) + } +} + +impl From for AriesVcxError { + fn from(err: ParseError) -> Self { + AriesVcxError::from_msg(AriesVcxErrorKind::InvalidInput, err.to_string()) + } +} diff --git a/aries/aries_vcx/src/handlers/out_of_band/receiver.rs b/aries/aries_vcx/src/handlers/out_of_band/receiver.rs index 6e19fe7914..ab1755e3bc 100644 --- a/aries/aries_vcx/src/handlers/out_of_band/receiver.rs +++ b/aries/aries_vcx/src/handlers/out_of_band/receiver.rs @@ -12,8 +12,11 @@ use messages::{ }; use serde::Deserialize; use serde_json::Value; +use url::Url; -use crate::{errors::error::prelude::*, handlers::util::AttachmentId}; +use crate::{ + errors::error::prelude::*, handlers::util::AttachmentId, utils::base64::URL_SAFE_LENIENT, +}; #[derive(Debug, PartialEq, Clone)] pub struct OutOfBandReceiver { @@ -38,6 +41,23 @@ impl OutOfBandReceiver { } } + pub fn create_from_json_encoded_oob(oob_json: &str) -> VcxResult { + Ok(Self { + oob: extract_encoded_invitation_from_json_string(oob_json)?, + }) + } + + pub fn create_from_url_encoded_oob(oob_url_string: &str) -> VcxResult { + // TODO - URL Shortening + Ok(Self { + oob: extract_encoded_invitation_from_json_string( + &extract_encoded_invitation_from_base64_url(&extract_encoded_invitation_from_url( + oob_url_string, + )?)?, + )?, + }) + } + pub fn get_id(&self) -> String { self.oob.id.clone() } @@ -58,17 +78,53 @@ impl OutOfBandReceiver { } } - pub fn to_aries_message(&self) -> AriesMessage { + pub fn invitation_to_aries_message(&self) -> AriesMessage { self.oob.clone().into() } - pub fn from_string(oob_data: &str) -> VcxResult { - Ok(Self { - oob: serde_json::from_str(oob_data)?, - }) + pub fn invitation_to_json_string(&self) -> String { + self.invitation_to_aries_message().to_string() + } + + fn invitation_to_base64_url(&self) -> String { + URL_SAFE_LENIENT.encode(self.invitation_to_json_string()) + } + + pub fn invitation_to_url(&self, domain_path: &str) -> VcxResult { + let oob_url = Url::parse(domain_path)? + .query_pairs_mut() + .append_pair("oob", &self.invitation_to_base64_url()) + .finish() + .to_owned(); + Ok(oob_url) } } +fn extract_encoded_invitation_from_json_string(oob_json: &str) -> VcxResult { + Ok(serde_json::from_str(oob_json)?) +} + +fn extract_encoded_invitation_from_base64_url(base64_url_encoded_oob: &str) -> VcxResult { + Ok(String::from_utf8( + URL_SAFE_LENIENT.decode(base64_url_encoded_oob)?, + )?) +} + +fn extract_encoded_invitation_from_url(oob_url_string: &str) -> VcxResult { + let oob_url = Url::parse(oob_url_string)?; + let (_oob_query, base64_url_encoded_oob) = oob_url + .query_pairs() + .find(|(name, _value)| name == "oob") + .ok_or_else(|| { + AriesVcxError::from_msg( + AriesVcxErrorKind::InvalidInput, + "OutOfBand Invitation URL is missing 'oob' query parameter", + ) + })?; + + Ok(base64_url_encoded_oob.into_owned()) +} + impl Display for OutOfBandReceiver { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", json!(AriesMessage::from(self.oob.clone()))) @@ -136,3 +192,149 @@ fn attachment_to_aries_message(attach: &Attachment) -> VcxResult Invitation { + let id = "69212a3a-d068-4f9d-a2dd-4741bca89af3"; + let did = "did:sov:LjgpST2rjsoxYegQDRm7EL"; + let service = OobService::Did(did.to_string()); + let handshake_protocols = vec![ + MaybeKnown::Known(Protocol::DidExchangeType(DidExchangeType::V1( + DidExchangeTypeV1::new_v1_0(), + ))), + MaybeKnown::Known(Protocol::ConnectionType(ConnectionType::V1( + ConnectionTypeV1::new_v1_0(), + ))), + ]; + let content = InvitationContent::builder() + .services(vec![service]) + .goal("To issue a Faber College Graduate credential".to_string()) + .goal_code(MaybeKnown::Known(OobGoalCode::IssueVC)) + .label("Faber College".to_string()) + .handshake_protocols(handshake_protocols) + .build(); + let decorators = InvitationDecorators::default(); + + let invitation: Invitation = Invitation::builder() + .id(id.to_string()) + .content(content) + .decorators(decorators) + .build(); + + invitation + } + + #[test] + fn receive_invitation_by_json() { + let base_invite = _create_invitation(); + let parsed_invite = OutOfBandReceiver::create_from_json_encoded_oob(JSON_OOB_INVITE) + .unwrap() + .oob; + assert_eq!(base_invite, parsed_invite); + } + + #[test] + fn receive_invitation_by_json_no_whitespace() { + let base_invite = _create_invitation(); + let parsed_invite = + OutOfBandReceiver::create_from_json_encoded_oob(JSON_OOB_INVITE_NO_WHITESPACE) + .unwrap() + .oob; + assert_eq!(base_invite, parsed_invite); + } + + #[test] + fn receive_invitation_by_url() { + let base_invite = _create_invitation(); + let parsed_invite = OutOfBandReceiver::create_from_url_encoded_oob(OOB_URL) + .unwrap() + .oob; + assert_eq!(base_invite, parsed_invite); + } + + #[test] + fn receive_invitation_by_url_with_padding() { + let base_invite = _create_invitation(); + let parsed_invite = OutOfBandReceiver::create_from_url_encoded_oob(OOB_URL_WITH_PADDING) + .unwrap() + .oob; + assert_eq!(base_invite, parsed_invite); + } + + #[test] + fn receive_invitation_by_url_with_padding_no_percent_encoding() { + let base_invite = _create_invitation(); + let parsed_invite = OutOfBandReceiver::create_from_url_encoded_oob( + OOB_URL_WITH_PADDING_NOT_PERCENT_ENCODED, + ) + .unwrap() + .oob; + assert_eq!(base_invite, parsed_invite); + } + + #[test] + fn invitation_to_json() { + let out_of_band_receiver = + OutOfBandReceiver::create_from_json_encoded_oob(JSON_OOB_INVITE).unwrap(); + + let json_invite = out_of_band_receiver.invitation_to_json_string(); + + assert_eq!(JSON_OOB_INVITE_NO_WHITESPACE, json_invite); + } + + #[test] + fn invitation_to_base64_url() { + let out_of_band_receiver = + OutOfBandReceiver::create_from_json_encoded_oob(JSON_OOB_INVITE).unwrap(); + + let base64_url_invite = out_of_band_receiver.invitation_to_base64_url(); + + assert_eq!(OOB_BASE64_URL_ENCODED, base64_url_invite); + } + + #[test] + fn invitation_to_url() { + let out_of_band_receiver = + OutOfBandReceiver::create_from_json_encoded_oob(JSON_OOB_INVITE).unwrap(); + + let oob_url = out_of_band_receiver + .invitation_to_url("http://example.com/ssi") + .unwrap() + .to_string(); + + assert_eq!(OOB_URL, oob_url); + } +} diff --git a/aries/aries_vcx/src/handlers/out_of_band/sender.rs b/aries/aries_vcx/src/handlers/out_of_band/sender.rs index 5ff935f7c0..efd5442fdd 100644 --- a/aries/aries_vcx/src/handlers/out_of_band/sender.rs +++ b/aries/aries_vcx/src/handlers/out_of_band/sender.rs @@ -1,5 +1,6 @@ use std::fmt::Display; +use base64::Engine; use messages::{ msg_fields::protocols::{ cred_issuance::{v1::CredentialIssuanceV1, CredentialIssuance}, @@ -13,11 +14,13 @@ use messages::{ AriesMessage, }; use shared::maybe_known::MaybeKnown; +use url::Url; use uuid::Uuid; use crate::{ errors::error::prelude::*, handlers::util::{make_attach_from_str, AttachmentId}, + utils::base64::URL_SAFE_LENIENT, }; #[derive(Debug, PartialEq, Clone)] @@ -40,6 +43,10 @@ impl OutOfBandSender { } } + pub fn create_from_invitation(invitation: Invitation) -> Self { + Self { oob: invitation } + } + pub fn set_label(mut self, label: &str) -> Self { self.oob.content.label = Some(label.to_string()); self @@ -124,14 +131,25 @@ impl OutOfBandSender { Ok(self) } - pub fn to_aries_message(&self) -> AriesMessage { + pub fn invitation_to_aries_message(&self) -> AriesMessage { self.oob.clone().into() } - pub fn from_string(oob_data: &str) -> VcxResult { - Ok(Self { - oob: serde_json::from_str(oob_data)?, - }) + pub fn invitation_to_json_string(&self) -> String { + self.invitation_to_aries_message().to_string() + } + + fn invitation_to_base64_url(&self) -> String { + URL_SAFE_LENIENT.encode(self.invitation_to_json_string()) + } + + pub fn invitation_to_url(&self, domain_path: &str) -> VcxResult { + let oob_url = Url::parse(domain_path)? + .query_pairs_mut() + .append_pair("oob", &self.invitation_to_base64_url()) + .finish() + .to_owned(); + Ok(oob_url) } } @@ -141,6 +159,90 @@ impl Display for OutOfBandSender { } } +#[cfg(test)] +mod tests { + use messages::{ + msg_fields::protocols::out_of_band::{ + invitation::{Invitation, InvitationContent, InvitationDecorators, OobService}, + OobGoalCode, + }, + msg_types::{ + connection::{ConnectionType, ConnectionTypeV1}, + protocols::did_exchange::{DidExchangeType, DidExchangeTypeV1}, + Protocol, + }, + }; + use shared::maybe_known::MaybeKnown; + + use super::*; + + // Example invite formats referenced (with change to use OOB 1.1) from example invite in RFC 0434 - https://github.com/hyperledger/aries-rfcs/tree/main/features/0434-outofband + const JSON_OOB_INVITE_NO_WHITESPACE: &str = r#"{"@type":"https://didcomm.org/out-of-band/1.1/invitation","@id":"69212a3a-d068-4f9d-a2dd-4741bca89af3","label":"Faber College","goal_code":"issue-vc","goal":"To issue a Faber College Graduate credential","handshake_protocols":["https://didcomm.org/didexchange/1.0","https://didcomm.org/connections/1.0"],"services":["did:sov:LjgpST2rjsoxYegQDRm7EL"]}"#; + const OOB_BASE64_URL_ENCODED: &str = "eyJAdHlwZSI6Imh0dHBzOi8vZGlkY29tbS5vcmcvb3V0LW9mLWJhbmQvMS4xL2ludml0YXRpb24iLCJAaWQiOiI2OTIxMmEzYS1kMDY4LTRmOWQtYTJkZC00NzQxYmNhODlhZjMiLCJsYWJlbCI6IkZhYmVyIENvbGxlZ2UiLCJnb2FsX2NvZGUiOiJpc3N1ZS12YyIsImdvYWwiOiJUbyBpc3N1ZSBhIEZhYmVyIENvbGxlZ2UgR3JhZHVhdGUgY3JlZGVudGlhbCIsImhhbmRzaGFrZV9wcm90b2NvbHMiOlsiaHR0cHM6Ly9kaWRjb21tLm9yZy9kaWRleGNoYW5nZS8xLjAiLCJodHRwczovL2RpZGNvbW0ub3JnL2Nvbm5lY3Rpb25zLzEuMCJdLCJzZXJ2aWNlcyI6WyJkaWQ6c292OkxqZ3BTVDJyanNveFllZ1FEUm03RUwiXX0"; + const OOB_URL: &str = "http://example.com/ssi?oob=eyJAdHlwZSI6Imh0dHBzOi8vZGlkY29tbS5vcmcvb3V0LW9mLWJhbmQvMS4xL2ludml0YXRpb24iLCJAaWQiOiI2OTIxMmEzYS1kMDY4LTRmOWQtYTJkZC00NzQxYmNhODlhZjMiLCJsYWJlbCI6IkZhYmVyIENvbGxlZ2UiLCJnb2FsX2NvZGUiOiJpc3N1ZS12YyIsImdvYWwiOiJUbyBpc3N1ZSBhIEZhYmVyIENvbGxlZ2UgR3JhZHVhdGUgY3JlZGVudGlhbCIsImhhbmRzaGFrZV9wcm90b2NvbHMiOlsiaHR0cHM6Ly9kaWRjb21tLm9yZy9kaWRleGNoYW5nZS8xLjAiLCJodHRwczovL2RpZGNvbW0ub3JnL2Nvbm5lY3Rpb25zLzEuMCJdLCJzZXJ2aWNlcyI6WyJkaWQ6c292OkxqZ3BTVDJyanNveFllZ1FEUm03RUwiXX0"; + + // Params mimic example invitation in RFC 0434 - https://github.com/hyperledger/aries-rfcs/tree/main/features/0434-outofband + fn _create_invitation() -> Invitation { + let id = "69212a3a-d068-4f9d-a2dd-4741bca89af3"; + let did = "did:sov:LjgpST2rjsoxYegQDRm7EL"; + let service = OobService::Did(did.to_string()); + let handshake_protocols = vec![ + MaybeKnown::Known(Protocol::DidExchangeType(DidExchangeType::V1( + DidExchangeTypeV1::new_v1_0(), + ))), + MaybeKnown::Known(Protocol::ConnectionType(ConnectionType::V1( + ConnectionTypeV1::new_v1_0(), + ))), + ]; + let content = InvitationContent::builder() + .services(vec![service]) + .goal("To issue a Faber College Graduate credential".to_string()) + .goal_code(MaybeKnown::Known(OobGoalCode::IssueVC)) + .label("Faber College".to_string()) + .handshake_protocols(handshake_protocols) + .build(); + let decorators = InvitationDecorators::default(); + + let invitation: Invitation = Invitation::builder() + .id(id.to_string()) + .content(content) + .decorators(decorators) + .build(); + + invitation + } + + #[test] + fn invitation_to_json() { + let out_of_band_sender = OutOfBandSender::create_from_invitation(_create_invitation()); + + let json_invite = out_of_band_sender.invitation_to_json_string(); + + assert_eq!(JSON_OOB_INVITE_NO_WHITESPACE, json_invite); + } + + #[test] + fn invitation_to_base64_url() { + let out_of_band_sender = OutOfBandSender::create_from_invitation(_create_invitation()); + + let base64_url_invite = out_of_band_sender.invitation_to_base64_url(); + + assert_eq!(OOB_BASE64_URL_ENCODED, base64_url_invite); + } + + #[test] + fn invitation_to_url() { + let out_of_band_sender = OutOfBandSender::create_from_invitation(_create_invitation()); + + let oob_url = out_of_band_sender + .invitation_to_url("http://example.com/ssi") + .unwrap() + .to_string(); + + assert_eq!(OOB_URL, oob_url); + } +} + // #[cfg(test)] // mod unit_tests { // use crate::utils::devsetup::SetupMocks;