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