2017-05-25 01:34:03 +00:00
|
|
|
|
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
|
|
|
//
|
|
|
|
|
// 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/.
|
|
|
|
|
|
2018-12-18 14:27:30 +00:00
|
|
|
|
use crate::data_forms::DataForm;
|
2018-12-18 14:32:05 +00:00
|
|
|
|
use crate::disco::{DiscoInfoQuery, DiscoInfoResult, Feature, Identity};
|
2019-01-13 11:39:51 +00:00
|
|
|
|
use crate::util::error::Error;
|
2018-12-18 14:32:05 +00:00
|
|
|
|
use crate::hashes::{Algo, Hash};
|
2018-12-18 14:27:30 +00:00
|
|
|
|
use crate::ns;
|
2018-12-18 14:32:05 +00:00
|
|
|
|
use crate::presence::PresencePayload;
|
|
|
|
|
use blake2::VarBlake2b;
|
|
|
|
|
use digest::{Digest, Input, VariableOutput};
|
|
|
|
|
use minidom::Element;
|
2017-11-15 18:37:28 +00:00
|
|
|
|
use sha1::Sha1;
|
2017-05-25 01:34:03 +00:00
|
|
|
|
use sha2::{Sha256, Sha512};
|
|
|
|
|
use sha3::{Sha3_256, Sha3_512};
|
2019-04-12 08:58:42 +00:00
|
|
|
|
use std::convert::TryFrom;
|
2017-05-25 01:34:03 +00:00
|
|
|
|
|
2018-08-08 17:12:09 +00:00
|
|
|
|
/// Represents a capability hash for a given client.
|
2017-05-25 01:34:03 +00:00
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct Caps {
|
2018-08-08 17:12:09 +00:00
|
|
|
|
/// Deprecated list of additional feature bundles.
|
2017-05-25 01:34:03 +00:00
|
|
|
|
pub ext: Option<String>,
|
2018-08-08 17:12:09 +00:00
|
|
|
|
|
|
|
|
|
/// A URI identifying an XMPP application.
|
2017-05-25 01:34:03 +00:00
|
|
|
|
pub node: String,
|
2018-08-08 17:12:09 +00:00
|
|
|
|
|
|
|
|
|
/// 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).
|
2017-05-25 01:34:03 +00:00
|
|
|
|
pub hash: Hash,
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-20 18:51:48 +00:00
|
|
|
|
impl PresencePayload for Caps {}
|
|
|
|
|
|
2017-05-25 01:34:03 +00:00
|
|
|
|
impl TryFrom<Element> for Caps {
|
2019-04-12 08:58:42 +00:00
|
|
|
|
type Error = Error;
|
2017-05-25 01:34:03 +00:00
|
|
|
|
|
|
|
|
|
fn try_from(elem: Element) -> Result<Caps, Error> {
|
2018-05-14 14:30:28 +00:00
|
|
|
|
check_self!(elem, "c", CAPS, "caps");
|
2017-10-10 17:04:27 +00:00
|
|
|
|
check_no_children!(elem, "caps");
|
|
|
|
|
check_no_unknown_attributes!(elem, "caps", ["hash", "ver", "ext", "node"]);
|
2019-02-24 19:48:19 +00:00
|
|
|
|
let ver: String = get_attr!(elem, "ver", Required);
|
2017-05-25 01:34:03 +00:00
|
|
|
|
let hash = Hash {
|
2019-02-24 19:48:19 +00:00
|
|
|
|
algo: get_attr!(elem, "hash", Required),
|
2017-05-25 01:34:03 +00:00
|
|
|
|
hash: base64::decode(&ver)?,
|
|
|
|
|
};
|
|
|
|
|
Ok(Caps {
|
2019-02-24 19:48:19 +00:00
|
|
|
|
ext: get_attr!(elem, "ext", Option),
|
|
|
|
|
node: get_attr!(elem, "node", Required),
|
2019-02-21 20:00:58 +00:00
|
|
|
|
hash,
|
2017-05-25 01:34:03 +00:00
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<Caps> for Element {
|
|
|
|
|
fn from(caps: Caps) -> Element {
|
|
|
|
|
Element::builder("c")
|
2018-12-18 14:32:05 +00:00
|
|
|
|
.ns(ns::CAPS)
|
|
|
|
|
.attr("ext", caps.ext)
|
|
|
|
|
.attr("hash", caps.hash.algo)
|
|
|
|
|
.attr("node", caps.node)
|
|
|
|
|
.attr("ver", base64::encode(&caps.hash.hash))
|
|
|
|
|
.build()
|
2017-05-25 01:34:03 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-27 16:20:34 +00:00
|
|
|
|
impl Caps {
|
|
|
|
|
/// Create a Caps element from its node and hash.
|
|
|
|
|
pub fn new<N: Into<String>>(node: N, hash: Hash) -> Caps {
|
|
|
|
|
Caps {
|
|
|
|
|
ext: None,
|
|
|
|
|
node: node.into(),
|
|
|
|
|
hash,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-25 01:34:03 +00:00
|
|
|
|
fn compute_item(field: &str) -> Vec<u8> {
|
|
|
|
|
let mut bytes = field.as_bytes().to_vec();
|
|
|
|
|
bytes.push(b'<');
|
|
|
|
|
bytes
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn compute_items<T, F: Fn(&T) -> Vec<u8>>(things: &[T], encode: F) -> Vec<u8> {
|
2018-12-18 14:32:05 +00:00
|
|
|
|
let mut string: Vec<u8> = vec![];
|
|
|
|
|
let mut accumulator: Vec<Vec<u8>> = vec![];
|
2017-05-25 01:34:03 +00:00
|
|
|
|
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<u8> {
|
|
|
|
|
compute_items(features, |feature| compute_item(&feature.var))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn compute_identities(identities: &[Identity]) -> Vec<u8> {
|
|
|
|
|
compute_items(identities, |identity| {
|
2017-05-27 21:10:00 +00:00
|
|
|
|
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);
|
2017-05-25 01:34:03 +00:00
|
|
|
|
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<u8> {
|
|
|
|
|
compute_items(extensions, |extension| {
|
2018-12-18 14:32:05 +00:00
|
|
|
|
let mut bytes = vec![];
|
2017-05-25 01:34:03 +00:00
|
|
|
|
// 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));
|
2018-12-18 14:32:05 +00:00
|
|
|
|
bytes.append(&mut compute_items(&field.values, |value| {
|
|
|
|
|
compute_item(value)
|
|
|
|
|
}));
|
2017-05-25 01:34:03 +00:00
|
|
|
|
}
|
|
|
|
|
bytes
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-08 17:12:09 +00:00
|
|
|
|
/// 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).
|
2017-07-20 16:39:59 +00:00
|
|
|
|
pub fn compute_disco(disco: &DiscoInfoResult) -> Vec<u8> {
|
2017-05-25 01:34:03 +00:00
|
|
|
|
let identities_string = compute_identities(&disco.identities);
|
|
|
|
|
let features_string = compute_features(&disco.features);
|
|
|
|
|
let extensions_string = compute_extensions(&disco.extensions);
|
|
|
|
|
|
2018-12-18 14:32:05 +00:00
|
|
|
|
let mut final_string = vec![];
|
2017-05-25 01:34:03 +00:00
|
|
|
|
final_string.extend(identities_string);
|
|
|
|
|
final_string.extend(features_string);
|
|
|
|
|
final_string.extend(extensions_string);
|
|
|
|
|
final_string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_hash_vec(hash: &[u8]) -> Vec<u8> {
|
|
|
|
|
let mut vec = Vec::with_capacity(hash.len());
|
|
|
|
|
vec.extend_from_slice(hash);
|
|
|
|
|
vec
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-08 17:12:09 +00:00
|
|
|
|
/// Hashes the result of [compute_disco()] with one of the supported [hash
|
|
|
|
|
/// algorithms](../hashes/enum.Algo.html).
|
2017-05-25 01:34:03 +00:00
|
|
|
|
pub fn hash_caps(data: &[u8], algo: Algo) -> Result<Hash, String> {
|
|
|
|
|
Ok(Hash {
|
|
|
|
|
hash: match algo {
|
|
|
|
|
Algo::Sha_1 => {
|
2017-11-15 23:16:37 +00:00
|
|
|
|
let hash = Sha1::digest(data);
|
2017-05-25 01:34:03 +00:00
|
|
|
|
get_hash_vec(hash.as_slice())
|
2018-12-18 14:32:05 +00:00
|
|
|
|
}
|
2017-05-25 01:34:03 +00:00
|
|
|
|
Algo::Sha_256 => {
|
2017-11-15 23:16:37 +00:00
|
|
|
|
let hash = Sha256::digest(data);
|
2017-05-25 01:34:03 +00:00
|
|
|
|
get_hash_vec(hash.as_slice())
|
2018-12-18 14:32:05 +00:00
|
|
|
|
}
|
2017-05-25 01:34:03 +00:00
|
|
|
|
Algo::Sha_512 => {
|
2017-11-15 23:16:37 +00:00
|
|
|
|
let hash = Sha512::digest(data);
|
2017-05-25 01:34:03 +00:00
|
|
|
|
get_hash_vec(hash.as_slice())
|
2018-12-18 14:32:05 +00:00
|
|
|
|
}
|
2017-05-25 01:34:03 +00:00
|
|
|
|
Algo::Sha3_256 => {
|
2017-11-15 23:16:37 +00:00
|
|
|
|
let hash = Sha3_256::digest(data);
|
2017-05-25 01:34:03 +00:00
|
|
|
|
get_hash_vec(hash.as_slice())
|
2018-12-18 14:32:05 +00:00
|
|
|
|
}
|
2017-05-25 01:34:03 +00:00
|
|
|
|
Algo::Sha3_512 => {
|
2017-11-15 23:16:37 +00:00
|
|
|
|
let hash = Sha3_512::digest(data);
|
2017-05-25 01:34:03 +00:00
|
|
|
|
get_hash_vec(hash.as_slice())
|
2018-12-18 14:32:05 +00:00
|
|
|
|
}
|
2017-05-25 01:34:03 +00:00
|
|
|
|
Algo::Blake2b_256 => {
|
2018-10-12 15:23:34 +00:00
|
|
|
|
let mut hasher = VarBlake2b::new(32).unwrap();
|
2017-05-25 01:34:03 +00:00
|
|
|
|
hasher.input(data);
|
2018-10-12 15:23:34 +00:00
|
|
|
|
hasher.vec_result()
|
2018-12-18 14:32:05 +00:00
|
|
|
|
}
|
2017-05-25 01:34:03 +00:00
|
|
|
|
Algo::Blake2b_512 => {
|
2018-10-12 15:23:34 +00:00
|
|
|
|
let mut hasher = VarBlake2b::new(64).unwrap();
|
2017-05-25 01:34:03 +00:00
|
|
|
|
hasher.input(data);
|
2018-10-12 15:23:34 +00:00
|
|
|
|
hasher.vec_result()
|
2018-12-18 14:32:05 +00:00
|
|
|
|
}
|
2017-05-25 01:34:03 +00:00
|
|
|
|
Algo::Unknown(algo) => return Err(format!("Unknown algorithm: {}.", algo)),
|
|
|
|
|
},
|
2019-02-21 20:00:58 +00:00
|
|
|
|
algo,
|
2017-05-25 01:34:03 +00:00
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-08 17:12:09 +00:00
|
|
|
|
/// Helper function to create the query for the disco#info corresponding to a
|
|
|
|
|
/// caps hash.
|
2017-07-20 16:39:59 +00:00
|
|
|
|
pub fn query_caps(caps: Caps) -> DiscoInfoQuery {
|
|
|
|
|
DiscoInfoQuery {
|
2017-05-27 11:22:50 +00:00
|
|
|
|
node: Some(format!("{}#{}", caps.node, base64::encode(&caps.hash.hash))),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-25 01:34:03 +00:00
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
2018-12-18 14:27:30 +00:00
|
|
|
|
use crate::caps;
|
2017-05-25 01:34:03 +00:00
|
|
|
|
|
2018-10-28 12:10:48 +00:00
|
|
|
|
#[cfg(target_pointer_width = "32")]
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_size() {
|
|
|
|
|
assert_size!(Caps, 52);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(target_pointer_width = "64")]
|
2018-10-26 12:26:16 +00:00
|
|
|
|
#[test]
|
|
|
|
|
fn test_size() {
|
|
|
|
|
assert_size!(Caps, 104);
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-25 01:34:03 +00:00
|
|
|
|
#[test]
|
|
|
|
|
fn test_parse() {
|
|
|
|
|
let elem: Element = "<c xmlns='http://jabber.org/protocol/caps' hash='sha-256' node='coucou' ver='K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4='/>".parse().unwrap();
|
|
|
|
|
let caps = Caps::try_from(elem).unwrap();
|
|
|
|
|
assert_eq!(caps.node, String::from("coucou"));
|
|
|
|
|
assert_eq!(caps.hash.algo, Algo::Sha_256);
|
2018-12-18 14:32:05 +00:00
|
|
|
|
assert_eq!(
|
|
|
|
|
caps.hash.hash,
|
|
|
|
|
base64::decode("K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=").unwrap()
|
|
|
|
|
);
|
2017-05-25 01:34:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-01-12 21:00:46 +00:00
|
|
|
|
#[cfg(not(feature = "disable-validation"))]
|
2017-05-25 01:34:03 +00:00
|
|
|
|
#[test]
|
|
|
|
|
fn test_invalid_child() {
|
|
|
|
|
let elem: Element = "<c xmlns='http://jabber.org/protocol/caps'><hash xmlns='urn:xmpp:hashes:2' algo='sha-256'>K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=</hash></c>".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 = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><feature var='http://jabber.org/protocol/disco#info'/></query>".parse().unwrap();
|
2017-07-20 16:39:59 +00:00
|
|
|
|
let disco = DiscoInfoResult::try_from(elem).unwrap();
|
2017-05-25 01:34:03 +00:00
|
|
|
|
let caps = caps::compute_disco(&disco);
|
|
|
|
|
assert_eq!(caps.len(), 50);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_xep_5_2() {
|
|
|
|
|
let elem: Element = r#"
|
|
|
|
|
<query xmlns='http://jabber.org/protocol/disco#info'
|
|
|
|
|
node='http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w='>
|
|
|
|
|
<identity category='client' name='Exodus 0.9.1' type='pc'/>
|
|
|
|
|
<feature var='http://jabber.org/protocol/caps'/>
|
|
|
|
|
<feature var='http://jabber.org/protocol/disco#info'/>
|
|
|
|
|
<feature var='http://jabber.org/protocol/disco#items'/>
|
|
|
|
|
<feature var='http://jabber.org/protocol/muc'/>
|
|
|
|
|
</query>
|
2018-12-18 14:32:05 +00:00
|
|
|
|
"#
|
|
|
|
|
.parse()
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
2017-05-25 01:34:03 +00:00
|
|
|
|
let data = b"client/pc//Exodus 0.9.1<http://jabber.org/protocol/caps<http://jabber.org/protocol/disco#info<http://jabber.org/protocol/disco#items<http://jabber.org/protocol/muc<";
|
|
|
|
|
let mut expected = Vec::with_capacity(data.len());
|
|
|
|
|
expected.extend_from_slice(data);
|
2017-07-20 16:39:59 +00:00
|
|
|
|
let disco = DiscoInfoResult::try_from(elem).unwrap();
|
2017-05-25 01:34:03 +00:00
|
|
|
|
let caps = caps::compute_disco(&disco);
|
|
|
|
|
assert_eq!(caps, expected);
|
|
|
|
|
|
|
|
|
|
let sha_1 = caps::hash_caps(&caps, Algo::Sha_1).unwrap();
|
2018-12-18 14:32:05 +00:00
|
|
|
|
assert_eq!(
|
|
|
|
|
sha_1.hash,
|
|
|
|
|
base64::decode("QgayPKawpkPSDYmwT/WM94uAlu0=").unwrap()
|
|
|
|
|
);
|
2017-05-25 01:34:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_xep_5_3() {
|
|
|
|
|
let elem: Element = r#"
|
|
|
|
|
<query xmlns='http://jabber.org/protocol/disco#info'
|
|
|
|
|
node='http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w='>
|
|
|
|
|
<identity xml:lang='en' category='client' name='Psi 0.11' type='pc'/>
|
|
|
|
|
<identity xml:lang='el' category='client' name='Ψ 0.11' type='pc'/>
|
|
|
|
|
<feature var='http://jabber.org/protocol/caps'/>
|
|
|
|
|
<feature var='http://jabber.org/protocol/disco#info'/>
|
|
|
|
|
<feature var='http://jabber.org/protocol/disco#items'/>
|
|
|
|
|
<feature var='http://jabber.org/protocol/muc'/>
|
|
|
|
|
<x xmlns='jabber:x:data' type='result'>
|
|
|
|
|
<field var='FORM_TYPE' type='hidden'>
|
|
|
|
|
<value>urn:xmpp:dataforms:softwareinfo</value>
|
|
|
|
|
</field>
|
|
|
|
|
<field var='ip_version'>
|
|
|
|
|
<value>ipv4</value>
|
|
|
|
|
<value>ipv6</value>
|
|
|
|
|
</field>
|
|
|
|
|
<field var='os'>
|
|
|
|
|
<value>Mac</value>
|
|
|
|
|
</field>
|
|
|
|
|
<field var='os_version'>
|
|
|
|
|
<value>10.5.1</value>
|
|
|
|
|
</field>
|
|
|
|
|
<field var='software'>
|
|
|
|
|
<value>Psi</value>
|
|
|
|
|
</field>
|
|
|
|
|
<field var='software_version'>
|
|
|
|
|
<value>0.11</value>
|
|
|
|
|
</field>
|
|
|
|
|
</x>
|
|
|
|
|
</query>
|
2018-12-18 14:32:05 +00:00
|
|
|
|
"#
|
|
|
|
|
.parse()
|
|
|
|
|
.unwrap();
|
2019-02-24 18:25:14 +00:00
|
|
|
|
let expected = b"client/pc/el/\xce\xa8 0.11<client/pc/en/Psi 0.11<http://jabber.org/protocol/caps<http://jabber.org/protocol/disco#info<http://jabber.org/protocol/disco#items<http://jabber.org/protocol/muc<urn:xmpp:dataforms:softwareinfo<ip_version<ipv4<ipv6<os<Mac<os_version<10.5.1<software<Psi<software_version<0.11<".to_vec();
|
2017-07-20 16:39:59 +00:00
|
|
|
|
let disco = DiscoInfoResult::try_from(elem).unwrap();
|
2017-05-25 01:34:03 +00:00
|
|
|
|
let caps = caps::compute_disco(&disco);
|
|
|
|
|
assert_eq!(caps, expected);
|
|
|
|
|
|
|
|
|
|
let sha_1 = caps::hash_caps(&caps, Algo::Sha_1).unwrap();
|
2018-12-18 14:32:05 +00:00
|
|
|
|
assert_eq!(
|
|
|
|
|
sha_1.hash,
|
|
|
|
|
base64::decode("q07IKJEyjvHSyhy//CH0CxmKi8w=").unwrap()
|
|
|
|
|
);
|
2017-05-25 01:34:03 +00:00
|
|
|
|
}
|
|
|
|
|
}
|