diff --git a/Cargo.toml b/Cargo.toml index a0d95aa..59a8a89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ minidom = "0.4.1" jid = "0.2.0" base64 = "0.5.0" digest = "0.5.0" +sha-1 = "0.3.0" sha2 = "0.5.0" sha3 = "0.5.0" blake2 = "0.5.0" diff --git a/src/caps.rs b/src/caps.rs new file mode 100644 index 0000000..d17cb50 --- /dev/null +++ b/src/caps.rs @@ -0,0 +1,296 @@ +// 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 std::convert::TryFrom; + +use disco::{Feature, Identity, Disco}; +use data_forms::DataForm; +use hashes::{Hash, Algo}; + +use minidom::Element; +use error::Error; +use ns; +use base64; + +use sha_1::Sha1; +use sha2::{Sha256, Sha512}; +use sha3::{Sha3_256, Sha3_512}; +use blake2::Blake2b; +use digest::{Digest, VariableOutput}; + +#[derive(Debug, Clone)] +pub struct Caps { + pub ext: Option, + pub node: String, + pub hash: Hash, +} + +impl TryFrom for Caps { + type Error = Error; + + fn try_from(elem: Element) -> Result { + if !elem.is("c", ns::CAPS) { + return Err(Error::ParseError("This is not a caps element.")); + } + for _ in elem.children() { + return Err(Error::ParseError("Unknown child in caps element.")); + } + let hash = get_attr!(elem, "hash", required); + let ver: String = get_attr!(elem, "ver", required); + let hash = Hash { + algo: hash, + 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 string = format!("{}/{}/{}/{}", identity.category, identity.type_, identity.xml_lang, match identity.name { Some(ref name) => name.clone(), None => String::new() }); + 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 + }) +} + +pub fn compute_disco(disco: &Disco) -> 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 +} + +pub fn hash_caps(data: &[u8], algo: Algo) -> Result { + Ok(Hash { + hash: match algo { + Algo::Sha_1 => { + let mut hasher = Sha1::default(); + hasher.input(data); + let hash = hasher.result(); + get_hash_vec(hash.as_slice()) + }, + Algo::Sha_256 => { + let mut hasher = Sha256::default(); + hasher.input(data); + let hash = hasher.result(); + get_hash_vec(hash.as_slice()) + }, + Algo::Sha_512 => { + let mut hasher = Sha512::default(); + hasher.input(data); + let hash = hasher.result(); + get_hash_vec(hash.as_slice()) + }, + Algo::Sha3_256 => { + let mut hasher = Sha3_256::default(); + hasher.input(data); + let hash = hasher.result(); + get_hash_vec(hash.as_slice()) + }, + Algo::Sha3_512 => { + let mut hasher = Sha3_512::default(); + hasher.input(data); + let hash = hasher.result(); + get_hash_vec(hash.as_slice()) + }, + Algo::Blake2b_256 => { + let mut hasher = Blake2b::default(); + hasher.input(data); + let mut buf: [u8; 32] = [0; 32]; + let hash = hasher.variable_result(&mut buf).unwrap(); + get_hash_vec(hash) + }, + Algo::Blake2b_512 => { + let mut hasher = Blake2b::default(); + hasher.input(data); + let mut buf: [u8; 64] = [0; 64]; + let hash = hasher.variable_result(&mut buf).unwrap(); + get_hash_vec(hash) + }, + Algo::Unknown(algo) => return Err(format!("Unknown algorithm: {}.", algo)), + }, + algo: algo, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use caps; + use base64; + + #[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 = Disco::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