// Copyright (c) 2017 Emmanuel Gil Peyrot // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. use try_from::TryFrom; use crate::presence::PresencePayload; use crate::disco::{Feature, Identity, DiscoInfoResult, DiscoInfoQuery}; use crate::data_forms::DataForm; use crate::hashes::{Hash, Algo}; use minidom::Element; use crate::error::Error; use crate::ns; use base64; use sha1::Sha1; use sha2::{Sha256, Sha512}; use sha3::{Sha3_256, Sha3_512}; use blake2::VarBlake2b; use digest::{Digest, VariableOutput, Input}; /// Represents a capability hash for a given client. #[derive(Debug, Clone)] pub struct Caps { /// Deprecated list of additional feature bundles. pub ext: Option, /// A URI identifying an XMPP application. pub node: String, /// The hash of that application’s /// [disco#info](../disco/struct.DiscoInfoResult.html). /// /// Warning: This protocol is insecure, you may want to switch to /// [ecaps2](../ecaps2/index.html) instead, see [this /// email](https://mail.jabber.org/pipermail/security/2009-July/000812.html). pub hash: Hash, } impl PresencePayload for Caps {} impl TryFrom for Caps { type Err = Error; fn try_from(elem: Element) -> Result { check_self!(elem, "c", CAPS, "caps"); check_no_children!(elem, "caps"); check_no_unknown_attributes!(elem, "caps", ["hash", "ver", "ext", "node"]); let ver: String = get_attr!(elem, "ver", required); let hash = Hash { algo: get_attr!(elem, "hash", required), hash: base64::decode(&ver)?, }; Ok(Caps { ext: get_attr!(elem, "ext", optional), node: get_attr!(elem, "node", required), hash: hash, }) } } impl From for Element { fn from(caps: Caps) -> Element { Element::builder("c") .ns(ns::CAPS) .attr("ext", caps.ext) .attr("hash", caps.hash.algo) .attr("node", caps.node) .attr("ver", base64::encode(&caps.hash.hash)) .build() } } fn compute_item(field: &str) -> Vec { let mut bytes = field.as_bytes().to_vec(); bytes.push(b'<'); bytes } fn compute_items Vec>(things: &[T], encode: F) -> Vec { let mut string: Vec = vec!(); let mut accumulator: Vec> = vec!(); for thing in things { let bytes = encode(thing); accumulator.push(bytes); } // This works using the expected i;octet collation. accumulator.sort(); for mut bytes in accumulator { string.append(&mut bytes); } string } fn compute_features(features: &[Feature]) -> Vec { compute_items(features, |feature| compute_item(&feature.var)) } fn compute_identities(identities: &[Identity]) -> Vec { compute_items(identities, |identity| { let lang = identity.lang.clone().unwrap_or_default(); let name = identity.name.clone().unwrap_or_default(); let string = format!("{}/{}/{}/{}", identity.category, identity.type_, lang, name); let bytes = string.as_bytes(); let mut vec = Vec::with_capacity(bytes.len()); vec.extend_from_slice(bytes); vec.push(b'<'); vec }) } fn compute_extensions(extensions: &[DataForm]) -> Vec { compute_items(extensions, |extension| { let mut bytes = vec!(); // TODO: maybe handle the error case? if let Some(ref form_type) = extension.form_type { bytes.extend_from_slice(form_type.as_bytes()); } bytes.push(b'<'); for field in extension.fields.clone() { if field.var == "FORM_TYPE" { continue; } bytes.append(&mut compute_item(&field.var)); bytes.append(&mut compute_items(&field.values, |value| compute_item(value))); } bytes }) } /// Applies the caps algorithm on the provided disco#info result, to generate /// the hash input. /// /// Warning: This protocol is insecure, you may want to switch to /// [ecaps2](../ecaps2/index.html) instead, see [this /// email](https://mail.jabber.org/pipermail/security/2009-July/000812.html). pub fn compute_disco(disco: &DiscoInfoResult) -> Vec { let identities_string = compute_identities(&disco.identities); let features_string = compute_features(&disco.features); let extensions_string = compute_extensions(&disco.extensions); let mut final_string = vec!(); final_string.extend(identities_string); final_string.extend(features_string); final_string.extend(extensions_string); final_string } fn get_hash_vec(hash: &[u8]) -> Vec { let mut vec = Vec::with_capacity(hash.len()); vec.extend_from_slice(hash); vec } /// Hashes the result of [compute_disco()] with one of the supported [hash /// algorithms](../hashes/enum.Algo.html). pub fn hash_caps(data: &[u8], algo: Algo) -> Result { Ok(Hash { hash: match algo { Algo::Sha_1 => { let hash = Sha1::digest(data); get_hash_vec(hash.as_slice()) }, Algo::Sha_256 => { let hash = Sha256::digest(data); get_hash_vec(hash.as_slice()) }, Algo::Sha_512 => { let hash = Sha512::digest(data); get_hash_vec(hash.as_slice()) }, Algo::Sha3_256 => { let hash = Sha3_256::digest(data); get_hash_vec(hash.as_slice()) }, Algo::Sha3_512 => { let hash = Sha3_512::digest(data); get_hash_vec(hash.as_slice()) }, Algo::Blake2b_256 => { let mut hasher = VarBlake2b::new(32).unwrap(); hasher.input(data); hasher.vec_result() }, Algo::Blake2b_512 => { let mut hasher = VarBlake2b::new(64).unwrap(); hasher.input(data); hasher.vec_result() }, Algo::Unknown(algo) => return Err(format!("Unknown algorithm: {}.", algo)), }, algo: algo, }) } /// Helper function to create the query for the disco#info corresponding to a /// caps hash. pub fn query_caps(caps: Caps) -> DiscoInfoQuery { DiscoInfoQuery { node: Some(format!("{}#{}", caps.node, base64::encode(&caps.hash.hash))), } } #[cfg(test)] mod tests { use super::*; use crate::caps; use base64; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Caps, 52); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Caps, 104); } #[test] fn test_parse() { let elem: Element = "".parse().unwrap(); let caps = Caps::try_from(elem).unwrap(); assert_eq!(caps.node, String::from("coucou")); assert_eq!(caps.hash.algo, Algo::Sha_256); assert_eq!(caps.hash.hash, base64::decode("K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=").unwrap()); } #[test] fn test_invalid_child() { let elem: Element = "K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=".parse().unwrap(); let error = Caps::try_from(elem).unwrap_err(); let message = match error { Error::ParseError(string) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in caps element."); } #[test] fn test_simple() { let elem: Element = "".parse().unwrap(); let disco = DiscoInfoResult::try_from(elem).unwrap(); let caps = caps::compute_disco(&disco); assert_eq!(caps.len(), 50); } #[test] fn test_xep_5_2() { let elem: Element = r#" "#.parse().unwrap(); let data = b"client/pc//Exodus 0.9.1 urn:xmpp:dataforms:softwareinfo ipv4 ipv6 Mac 10.5.1 Psi 0.11 "#.parse().unwrap(); let data = b"client/pc/el/\xce\xa8 0.11