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