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