Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add additional formats for parsing and outputting Out Of Band Invitations #1281

Merged
merged 10 commits into from
Aug 22, 2024
22 changes: 21 additions & 1 deletion aries/aries_vcx/src/errors/mapping_others.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand Down Expand Up @@ -92,3 +94,21 @@ impl From<ParseIntError> for AriesVcxError {
AriesVcxError::from_msg(AriesVcxErrorKind::InvalidInput, err.to_string())
}
}

impl From<DecodeError> for AriesVcxError {
fn from(err: DecodeError) -> Self {
AriesVcxError::from_msg(AriesVcxErrorKind::InvalidInput, err.to_string())
}
}

impl From<FromUtf8Error> for AriesVcxError {
fn from(err: FromUtf8Error) -> Self {
AriesVcxError::from_msg(AriesVcxErrorKind::InvalidInput, err.to_string())
}
}

impl From<ParseError> for AriesVcxError {
fn from(err: ParseError) -> Self {
AriesVcxError::from_msg(AriesVcxErrorKind::InvalidInput, err.to_string())
}
}
195 changes: 189 additions & 6 deletions aries/aries_vcx/src/handlers/out_of_band/receiver.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{clone::Clone, fmt::Display, str::FromStr};

use base64::{engine::general_purpose, Engine};
use base64::{engine::general_purpose, prelude::BASE64_URL_SAFE, Engine};
use messages::{
decorators::attachment::{Attachment, AttachmentType},
msg_fields::protocols::{
Expand All @@ -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 {
Expand All @@ -38,6 +41,25 @@ impl OutOfBandReceiver {
}
}

pub fn from_json_string(oob_json: &str) -> VcxResult<Self> {
Ok(Self {
oob: from_json_string(oob_json)?,
})
}

pub fn from_base64_url(base64_url_encoded_oob: &str) -> VcxResult<Self> {
gmulhearn marked this conversation as resolved.
Show resolved Hide resolved
Ok(Self {
oob: from_json_string(&from_base64_url(base64_url_encoded_oob)?)?,
})
}

pub fn from_url(oob_url_string: &str) -> VcxResult<Self> {
// TODO - URL Shortening
Ok(Self {
oob: from_json_string(&from_base64_url(&from_url(oob_url_string)?)?)?,
})
}

pub fn get_id(&self) -> String {
self.oob.id.clone()
}
Expand All @@ -62,11 +84,45 @@ impl OutOfBandReceiver {
self.oob.clone().into()
}

pub fn from_string(oob_data: &str) -> VcxResult<Self> {
Ok(Self {
oob: serde_json::from_str(oob_data)?,
})
pub fn to_json_string(&self) -> String {
self.to_aries_message().to_string()
}

pub fn to_base64_url(&self) -> String {
BASE64_URL_SAFE.encode(self.to_json_string())
}

pub fn to_url(&self, domain_path: &str) -> VcxResult<Url> {
let mut oob_url = Url::parse(domain_path)?;
let oob_query = "oob=".to_owned() + &self.to_base64_url();
oob_url.set_query(Some(&oob_query));
gmulhearn marked this conversation as resolved.
Show resolved Hide resolved
Ok(oob_url)
}
}

fn from_json_string(oob_json: &str) -> VcxResult<Invitation> {
Ok(serde_json::from_str(oob_json)?)
}

fn from_base64_url(base64_url_encoded_oob: &str) -> VcxResult<String> {
Ok(String::from_utf8(
URL_SAFE_LENIENT.decode(base64_url_encoded_oob)?,
JamesKEbert marked this conversation as resolved.
Show resolved Hide resolved
)?)
}

fn from_url(oob_url_string: &str) -> VcxResult<String> {
gmulhearn marked this conversation as resolved.
Show resolved Hide resolved
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 {
Expand Down Expand Up @@ -136,3 +192,130 @@ fn attachment_to_aries_message(attach: &Attachment) -> VcxResult<Option<AriesMes
)),
}
}

#[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: &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 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 receive_invitation_by_json() {
let base_invite = _create_invitation();
let parsed_invite = OutOfBandReceiver::from_json_string(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::from_json_string(JSON_OOB_INVITE_NO_WHITESPACE)
.unwrap()
.oob;
assert_eq!(base_invite, parsed_invite);
}

#[test]
fn receive_invitation_by_base64_url() {
let base_invite = _create_invitation();
let parsed_invite = OutOfBandReceiver::from_base64_url(OOB_BASE64_URL_ENCODED)
.unwrap()
.oob;
assert_eq!(base_invite, parsed_invite);
}

#[test]
fn receive_invitation_by_url() {
let base_invite = _create_invitation();
let parsed_invite = OutOfBandReceiver::from_url(OOB_URL).unwrap().oob;
assert_eq!(base_invite, parsed_invite);
}

#[test]
fn invitation_to_json() {
let out_of_band_receiver = OutOfBandReceiver::from_json_string(JSON_OOB_INVITE).unwrap();

let json_invite = out_of_band_receiver.to_json_string();

assert_eq!(JSON_OOB_INVITE_NO_WHITESPACE, json_invite);
}

#[test]
fn invitation_to_base64_url() {
let out_of_band_receiver = OutOfBandReceiver::from_json_string(JSON_OOB_INVITE).unwrap();

let base64_url_invite = out_of_band_receiver.to_base64_url();

assert_eq!(OOB_BASE64_URL_ENCODED, base64_url_invite);
}

#[test]
fn invitation_to_url() {
let out_of_band_receiver = OutOfBandReceiver::from_json_string(JSON_OOB_INVITE).unwrap();

let oob_url = out_of_band_receiver
.to_url("http://example.com/ssi")
.unwrap()
.to_string();

assert_eq!(OOB_URL, oob_url);
}
}
Loading
Loading