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::util::error::Error;
10use crate::hashes::{Algo, Hash};
11use crate::ns;
12use crate::presence::PresencePayload;
13use base64;
14use blake2::VarBlake2b;
15use digest::{Digest, Input, VariableOutput};
16use minidom::Element;
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")
65 .ns(ns::CAPS)
66 .attr("ext", caps.ext)
67 .attr("hash", caps.hash.algo)
68 .attr("node", caps.node)
69 .attr("ver", base64::encode(&caps.hash.hash))
70 .build()
71 }
72}
73
74impl Caps {
75 /// Create a Caps element from its node and hash.
76 pub fn new<N: Into<String>>(node: N, hash: Hash) -> Caps {
77 Caps {
78 ext: None,
79 node: node.into(),
80 hash,
81 }
82 }
83}
84
85fn compute_item(field: &str) -> Vec<u8> {
86 let mut bytes = field.as_bytes().to_vec();
87 bytes.push(b'<');
88 bytes
89}
90
91fn compute_items<T, F: Fn(&T) -> Vec<u8>>(things: &[T], encode: F) -> Vec<u8> {
92 let mut string: Vec<u8> = vec![];
93 let mut accumulator: Vec<Vec<u8>> = vec![];
94 for thing in things {
95 let bytes = encode(thing);
96 accumulator.push(bytes);
97 }
98 // This works using the expected i;octet collation.
99 accumulator.sort();
100 for mut bytes in accumulator {
101 string.append(&mut bytes);
102 }
103 string
104}
105
106fn compute_features(features: &[Feature]) -> Vec<u8> {
107 compute_items(features, |feature| compute_item(&feature.var))
108}
109
110fn compute_identities(identities: &[Identity]) -> Vec<u8> {
111 compute_items(identities, |identity| {
112 let lang = identity.lang.clone().unwrap_or_default();
113 let name = identity.name.clone().unwrap_or_default();
114 let string = format!("{}/{}/{}/{}", identity.category, identity.type_, lang, name);
115 let bytes = string.as_bytes();
116 let mut vec = Vec::with_capacity(bytes.len());
117 vec.extend_from_slice(bytes);
118 vec.push(b'<');
119 vec
120 })
121}
122
123fn compute_extensions(extensions: &[DataForm]) -> Vec<u8> {
124 compute_items(extensions, |extension| {
125 let mut bytes = vec![];
126 // TODO: maybe handle the error case?
127 if let Some(ref form_type) = extension.form_type {
128 bytes.extend_from_slice(form_type.as_bytes());
129 }
130 bytes.push(b'<');
131 for field in extension.fields.clone() {
132 if field.var == "FORM_TYPE" {
133 continue;
134 }
135 bytes.append(&mut compute_item(&field.var));
136 bytes.append(&mut compute_items(&field.values, |value| {
137 compute_item(value)
138 }));
139 }
140 bytes
141 })
142}
143
144/// Applies the caps algorithm on the provided disco#info result, to generate
145/// the hash input.
146///
147/// Warning: This protocol is insecure, you may want to switch to
148/// [ecaps2](../ecaps2/index.html) instead, see [this
149/// email](https://mail.jabber.org/pipermail/security/2009-July/000812.html).
150pub fn compute_disco(disco: &DiscoInfoResult) -> Vec<u8> {
151 let identities_string = compute_identities(&disco.identities);
152 let features_string = compute_features(&disco.features);
153 let extensions_string = compute_extensions(&disco.extensions);
154
155 let mut final_string = vec![];
156 final_string.extend(identities_string);
157 final_string.extend(features_string);
158 final_string.extend(extensions_string);
159 final_string
160}
161
162fn get_hash_vec(hash: &[u8]) -> Vec<u8> {
163 let mut vec = Vec::with_capacity(hash.len());
164 vec.extend_from_slice(hash);
165 vec
166}
167
168/// Hashes the result of [compute_disco()] with one of the supported [hash
169/// algorithms](../hashes/enum.Algo.html).
170pub fn hash_caps(data: &[u8], algo: Algo) -> Result<Hash, String> {
171 Ok(Hash {
172 hash: match algo {
173 Algo::Sha_1 => {
174 let hash = Sha1::digest(data);
175 get_hash_vec(hash.as_slice())
176 }
177 Algo::Sha_256 => {
178 let hash = Sha256::digest(data);
179 get_hash_vec(hash.as_slice())
180 }
181 Algo::Sha_512 => {
182 let hash = Sha512::digest(data);
183 get_hash_vec(hash.as_slice())
184 }
185 Algo::Sha3_256 => {
186 let hash = Sha3_256::digest(data);
187 get_hash_vec(hash.as_slice())
188 }
189 Algo::Sha3_512 => {
190 let hash = Sha3_512::digest(data);
191 get_hash_vec(hash.as_slice())
192 }
193 Algo::Blake2b_256 => {
194 let mut hasher = VarBlake2b::new(32).unwrap();
195 hasher.input(data);
196 hasher.vec_result()
197 }
198 Algo::Blake2b_512 => {
199 let mut hasher = VarBlake2b::new(64).unwrap();
200 hasher.input(data);
201 hasher.vec_result()
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 use base64;
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::decode("K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=").unwrap()
244 );
245 }
246
247 #[cfg(not(feature = "disable-validation"))]
248 #[test]
249 fn test_invalid_child() {
250 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();
251 let error = Caps::try_from(elem).unwrap_err();
252 let message = match error {
253 Error::ParseError(string) => string,
254 _ => panic!(),
255 };
256 assert_eq!(message, "Unknown child in caps element.");
257 }
258
259 #[test]
260 fn test_simple() {
261 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();
262 let disco = DiscoInfoResult::try_from(elem).unwrap();
263 let caps = caps::compute_disco(&disco);
264 assert_eq!(caps.len(), 50);
265 }
266
267 #[test]
268 fn test_xep_5_2() {
269 let elem: Element = r#"
270<query xmlns='http://jabber.org/protocol/disco#info'
271 node='http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w='>
272 <identity category='client' name='Exodus 0.9.1' type='pc'/>
273 <feature var='http://jabber.org/protocol/caps'/>
274 <feature var='http://jabber.org/protocol/disco#info'/>
275 <feature var='http://jabber.org/protocol/disco#items'/>
276 <feature var='http://jabber.org/protocol/muc'/>
277</query>
278"#
279 .parse()
280 .unwrap();
281
282 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<";
283 let mut expected = Vec::with_capacity(data.len());
284 expected.extend_from_slice(data);
285 let disco = DiscoInfoResult::try_from(elem).unwrap();
286 let caps = caps::compute_disco(&disco);
287 assert_eq!(caps, expected);
288
289 let sha_1 = caps::hash_caps(&caps, Algo::Sha_1).unwrap();
290 assert_eq!(
291 sha_1.hash,
292 base64::decode("QgayPKawpkPSDYmwT/WM94uAlu0=").unwrap()
293 );
294 }
295
296 #[test]
297 fn test_xep_5_3() {
298 let elem: Element = r#"
299<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}