caps.rs

  1// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
  2//
  3// This Source Code Form is subject to the terms of the Mozilla Public
  4// License, v. 2.0. If a copy of the MPL was not distributed with this
  5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
  6
  7use try_from::TryFrom;
  8
  9use crate::presence::PresencePayload;
 10use crate::disco::{Feature, Identity, DiscoInfoResult, DiscoInfoQuery};
 11use crate::data_forms::DataForm;
 12use crate::hashes::{Hash, Algo};
 13
 14use minidom::Element;
 15use crate::error::Error;
 16use crate::ns;
 17use base64;
 18
 19use sha1::Sha1;
 20use sha2::{Sha256, Sha512};
 21use sha3::{Sha3_256, Sha3_512};
 22use blake2::VarBlake2b;
 23use digest::{Digest, VariableOutput, Input};
 24
 25/// Represents a capability hash for a given client.
 26#[derive(Debug, Clone)]
 27pub struct Caps {
 28    /// Deprecated list of additional feature bundles.
 29    pub ext: Option<String>,
 30
 31    /// A URI identifying an XMPP application.
 32    pub node: String,
 33
 34    /// The hash of that application’s
 35    /// [disco#info](../disco/struct.DiscoInfoResult.html).
 36    ///
 37    /// Warning: This protocol is insecure, you may want to switch to
 38    /// [ecaps2](../ecaps2/index.html) instead, see [this
 39    /// email](https://mail.jabber.org/pipermail/security/2009-July/000812.html).
 40    pub hash: Hash,
 41}
 42
 43impl PresencePayload for Caps {}
 44
 45impl TryFrom<Element> for Caps {
 46    type Err = Error;
 47
 48    fn try_from(elem: Element) -> Result<Caps, Error> {
 49        check_self!(elem, "c", CAPS, "caps");
 50        check_no_children!(elem, "caps");
 51        check_no_unknown_attributes!(elem, "caps", ["hash", "ver", "ext", "node"]);
 52        let ver: String = get_attr!(elem, "ver", required);
 53        let hash = Hash {
 54            algo: get_attr!(elem, "hash", required),
 55            hash: base64::decode(&ver)?,
 56        };
 57        Ok(Caps {
 58            ext: get_attr!(elem, "ext", optional),
 59            node: get_attr!(elem, "node", required),
 60            hash: hash,
 61        })
 62    }
 63}
 64
 65impl From<Caps> for Element {
 66    fn from(caps: Caps) -> Element {
 67        Element::builder("c")
 68                .ns(ns::CAPS)
 69                .attr("ext", caps.ext)
 70                .attr("hash", caps.hash.algo)
 71                .attr("node", caps.node)
 72                .attr("ver", base64::encode(&caps.hash.hash))
 73                .build()
 74    }
 75}
 76
 77fn compute_item(field: &str) -> Vec<u8> {
 78    let mut bytes = field.as_bytes().to_vec();
 79    bytes.push(b'<');
 80    bytes
 81}
 82
 83fn compute_items<T, F: Fn(&T) -> Vec<u8>>(things: &[T], encode: F) -> Vec<u8> {
 84    let mut string: Vec<u8> = vec!();
 85    let mut accumulator: Vec<Vec<u8>> = vec!();
 86    for thing in things {
 87        let bytes = encode(thing);
 88        accumulator.push(bytes);
 89    }
 90    // This works using the expected i;octet collation.
 91    accumulator.sort();
 92    for mut bytes in accumulator {
 93        string.append(&mut bytes);
 94    }
 95    string
 96}
 97
 98fn compute_features(features: &[Feature]) -> Vec<u8> {
 99    compute_items(features, |feature| compute_item(&feature.var))
100}
101
102fn compute_identities(identities: &[Identity]) -> Vec<u8> {
103    compute_items(identities, |identity| {
104        let lang = identity.lang.clone().unwrap_or_default();
105        let name = identity.name.clone().unwrap_or_default();
106        let string = format!("{}/{}/{}/{}", identity.category, identity.type_, lang, name);
107        let bytes = string.as_bytes();
108        let mut vec = Vec::with_capacity(bytes.len());
109        vec.extend_from_slice(bytes);
110        vec.push(b'<');
111        vec
112    })
113}
114
115fn compute_extensions(extensions: &[DataForm]) -> Vec<u8> {
116    compute_items(extensions, |extension| {
117        let mut bytes = vec!();
118        // TODO: maybe handle the error case?
119        if let Some(ref form_type) = extension.form_type {
120            bytes.extend_from_slice(form_type.as_bytes());
121        }
122        bytes.push(b'<');
123        for field in extension.fields.clone() {
124            if field.var == "FORM_TYPE" {
125                continue;
126            }
127            bytes.append(&mut compute_item(&field.var));
128            bytes.append(&mut compute_items(&field.values,
129                                            |value| compute_item(value)));
130        }
131        bytes
132    })
133}
134
135/// Applies the caps algorithm on the provided disco#info result, to generate
136/// the hash input.
137///
138/// Warning: This protocol is insecure, you may want to switch to
139/// [ecaps2](../ecaps2/index.html) instead, see [this
140/// email](https://mail.jabber.org/pipermail/security/2009-July/000812.html).
141pub fn compute_disco(disco: &DiscoInfoResult) -> Vec<u8> {
142    let identities_string = compute_identities(&disco.identities);
143    let features_string = compute_features(&disco.features);
144    let extensions_string = compute_extensions(&disco.extensions);
145
146    let mut final_string = vec!();
147    final_string.extend(identities_string);
148    final_string.extend(features_string);
149    final_string.extend(extensions_string);
150    final_string
151}
152
153fn get_hash_vec(hash: &[u8]) -> Vec<u8> {
154    let mut vec = Vec::with_capacity(hash.len());
155    vec.extend_from_slice(hash);
156    vec
157}
158
159/// Hashes the result of [compute_disco()] with one of the supported [hash
160/// algorithms](../hashes/enum.Algo.html).
161pub fn hash_caps(data: &[u8], algo: Algo) -> Result<Hash, String> {
162    Ok(Hash {
163        hash: match algo {
164            Algo::Sha_1 => {
165                let hash = Sha1::digest(data);
166                get_hash_vec(hash.as_slice())
167            },
168            Algo::Sha_256 => {
169                let hash = Sha256::digest(data);
170                get_hash_vec(hash.as_slice())
171            },
172            Algo::Sha_512 => {
173                let hash = Sha512::digest(data);
174                get_hash_vec(hash.as_slice())
175            },
176            Algo::Sha3_256 => {
177                let hash = Sha3_256::digest(data);
178                get_hash_vec(hash.as_slice())
179            },
180            Algo::Sha3_512 => {
181                let hash = Sha3_512::digest(data);
182                get_hash_vec(hash.as_slice())
183            },
184            Algo::Blake2b_256 => {
185                let mut hasher = VarBlake2b::new(32).unwrap();
186                hasher.input(data);
187                hasher.vec_result()
188            },
189            Algo::Blake2b_512 => {
190                let mut hasher = VarBlake2b::new(64).unwrap();
191                hasher.input(data);
192                hasher.vec_result()
193            },
194            Algo::Unknown(algo) => return Err(format!("Unknown algorithm: {}.", algo)),
195        },
196        algo: algo,
197    })
198}
199
200/// Helper function to create the query for the disco#info corresponding to a
201/// caps hash.
202pub fn query_caps(caps: Caps) -> DiscoInfoQuery {
203    DiscoInfoQuery {
204        node: Some(format!("{}#{}", caps.node, base64::encode(&caps.hash.hash))),
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use crate::caps;
212    use base64;
213
214    #[cfg(target_pointer_width = "32")]
215    #[test]
216    fn test_size() {
217        assert_size!(Caps, 52);
218    }
219
220    #[cfg(target_pointer_width = "64")]
221    #[test]
222    fn test_size() {
223        assert_size!(Caps, 104);
224    }
225
226    #[test]
227    fn test_parse() {
228        let elem: Element = "<c xmlns='http://jabber.org/protocol/caps' hash='sha-256' node='coucou' ver='K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4='/>".parse().unwrap();
229        let caps = Caps::try_from(elem).unwrap();
230        assert_eq!(caps.node, String::from("coucou"));
231        assert_eq!(caps.hash.algo, Algo::Sha_256);
232        assert_eq!(caps.hash.hash, base64::decode("K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=").unwrap());
233    }
234
235    #[test]
236    fn test_invalid_child() {
237        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();
238        let error = Caps::try_from(elem).unwrap_err();
239        let message = match error {
240            Error::ParseError(string) => string,
241            _ => panic!(),
242        };
243        assert_eq!(message, "Unknown child in caps element.");
244    }
245
246    #[test]
247    fn test_simple() {
248        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();
249        let disco = DiscoInfoResult::try_from(elem).unwrap();
250        let caps = caps::compute_disco(&disco);
251        assert_eq!(caps.len(), 50);
252    }
253
254    #[test]
255    fn test_xep_5_2() {
256        let elem: Element = r#"
257<query xmlns='http://jabber.org/protocol/disco#info'
258       node='http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w='>
259  <identity category='client' name='Exodus 0.9.1' type='pc'/>
260  <feature var='http://jabber.org/protocol/caps'/>
261  <feature var='http://jabber.org/protocol/disco#info'/>
262  <feature var='http://jabber.org/protocol/disco#items'/>
263  <feature var='http://jabber.org/protocol/muc'/>
264</query>
265"#.parse().unwrap();
266        
267        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<";
268        let mut expected = Vec::with_capacity(data.len());
269        expected.extend_from_slice(data);
270        let disco = DiscoInfoResult::try_from(elem).unwrap();
271        let caps = caps::compute_disco(&disco);
272        assert_eq!(caps, expected);
273
274        let sha_1 = caps::hash_caps(&caps, Algo::Sha_1).unwrap();
275        assert_eq!(sha_1.hash, base64::decode("QgayPKawpkPSDYmwT/WM94uAlu0=").unwrap());
276    }
277
278    #[test]
279    fn test_xep_5_3() {
280        let elem: Element = r#"
281<query xmlns='http://jabber.org/protocol/disco#info'
282       node='http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w='>
283  <identity xml:lang='en' category='client' name='Psi 0.11' type='pc'/>
284  <identity xml:lang='el' category='client' name='Ψ 0.11' type='pc'/>
285  <feature var='http://jabber.org/protocol/caps'/>
286  <feature var='http://jabber.org/protocol/disco#info'/>
287  <feature var='http://jabber.org/protocol/disco#items'/>
288  <feature var='http://jabber.org/protocol/muc'/>
289  <x xmlns='jabber:x:data' type='result'>
290    <field var='FORM_TYPE' type='hidden'>
291      <value>urn:xmpp:dataforms:softwareinfo</value>
292    </field>
293    <field var='ip_version'>
294      <value>ipv4</value>
295      <value>ipv6</value>
296    </field>
297    <field var='os'>
298      <value>Mac</value>
299    </field>
300    <field var='os_version'>
301      <value>10.5.1</value>
302    </field>
303    <field var='software'>
304      <value>Psi</value>
305    </field>
306    <field var='software_version'>
307      <value>0.11</value>
308    </field>
309  </x>
310</query>
311"#.parse().unwrap();
312        let data = 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<";
313        let mut expected = Vec::with_capacity(data.len());
314        expected.extend_from_slice(data);
315        let disco = DiscoInfoResult::try_from(elem).unwrap();
316        let caps = caps::compute_disco(&disco);
317        assert_eq!(caps, expected);
318
319        let sha_1 = caps::hash_caps(&caps, Algo::Sha_1).unwrap();
320        assert_eq!(sha_1.hash, base64::decode("q07IKJEyjvHSyhy//CH0CxmKi8w=").unwrap());
321    }
322}