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 blake2::VarBlake2b;
14use digest::{Digest, Input, VariableOutput};
15use crate::Element;
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")
64 .ns(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 let mut vec = Vec::with_capacity(hash.len());
163 vec.extend_from_slice(hash);
164 vec
165}
166
167/// Hashes the result of [compute_disco()] with one of the supported [hash
168/// algorithms](../hashes/enum.Algo.html).
169pub fn hash_caps(data: &[u8], algo: Algo) -> Result<Hash, String> {
170 Ok(Hash {
171 hash: match algo {
172 Algo::Sha_1 => {
173 let hash = Sha1::digest(data);
174 get_hash_vec(hash.as_slice())
175 }
176 Algo::Sha_256 => {
177 let hash = Sha256::digest(data);
178 get_hash_vec(hash.as_slice())
179 }
180 Algo::Sha_512 => {
181 let hash = Sha512::digest(data);
182 get_hash_vec(hash.as_slice())
183 }
184 Algo::Sha3_256 => {
185 let hash = Sha3_256::digest(data);
186 get_hash_vec(hash.as_slice())
187 }
188 Algo::Sha3_512 => {
189 let hash = Sha3_512::digest(data);
190 get_hash_vec(hash.as_slice())
191 }
192 Algo::Blake2b_256 => {
193 let mut hasher = VarBlake2b::new(32).unwrap();
194 hasher.input(data);
195 hasher.vec_result()
196 }
197 Algo::Blake2b_512 => {
198 let mut hasher = VarBlake2b::new(64).unwrap();
199 hasher.input(data);
200 hasher.vec_result()
201 }
202 Algo::Unknown(algo) => return Err(format!("Unknown algorithm: {}.", algo)),
203 },
204 algo,
205 })
206}
207
208/// Helper function to create the query for the disco#info corresponding to a
209/// caps hash.
210pub fn query_caps(caps: Caps) -> DiscoInfoQuery {
211 DiscoInfoQuery {
212 node: Some(format!("{}#{}", caps.node, base64::encode(&caps.hash.hash))),
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use crate::caps;
220
221 #[cfg(target_pointer_width = "32")]
222 #[test]
223 fn test_size() {
224 assert_size!(Caps, 52);
225 }
226
227 #[cfg(target_pointer_width = "64")]
228 #[test]
229 fn test_size() {
230 assert_size!(Caps, 104);
231 }
232
233 #[test]
234 fn test_parse() {
235 let elem: Element = "<c xmlns='http://jabber.org/protocol/caps' hash='sha-256' node='coucou' ver='K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4='/>".parse().unwrap();
236 let caps = Caps::try_from(elem).unwrap();
237 assert_eq!(caps.node, String::from("coucou"));
238 assert_eq!(caps.hash.algo, Algo::Sha_256);
239 assert_eq!(
240 caps.hash.hash,
241 base64::decode("K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=").unwrap()
242 );
243 }
244
245 #[cfg(not(feature = "disable-validation"))]
246 #[test]
247 fn test_invalid_child() {
248 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();
249 let error = Caps::try_from(elem).unwrap_err();
250 let message = match error {
251 Error::ParseError(string) => string,
252 _ => panic!(),
253 };
254 assert_eq!(message, "Unknown child in caps element.");
255 }
256
257 #[test]
258 fn test_simple() {
259 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();
260 let disco = DiscoInfoResult::try_from(elem).unwrap();
261 let caps = caps::compute_disco(&disco);
262 assert_eq!(caps.len(), 50);
263 }
264
265 #[test]
266 fn test_xep_5_2() {
267 let elem: Element = r#"
268<query xmlns='http://jabber.org/protocol/disco#info'
269 node='http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w='>
270 <identity category='client' name='Exodus 0.9.1' type='pc'/>
271 <feature var='http://jabber.org/protocol/caps'/>
272 <feature var='http://jabber.org/protocol/disco#info'/>
273 <feature var='http://jabber.org/protocol/disco#items'/>
274 <feature var='http://jabber.org/protocol/muc'/>
275</query>
276"#
277 .parse()
278 .unwrap();
279
280 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<";
281 let mut expected = Vec::with_capacity(data.len());
282 expected.extend_from_slice(data);
283 let disco = DiscoInfoResult::try_from(elem).unwrap();
284 let caps = caps::compute_disco(&disco);
285 assert_eq!(caps, expected);
286
287 let sha_1 = caps::hash_caps(&caps, Algo::Sha_1).unwrap();
288 assert_eq!(
289 sha_1.hash,
290 base64::decode("QgayPKawpkPSDYmwT/WM94uAlu0=").unwrap()
291 );
292 }
293
294 #[test]
295 fn test_xep_5_3() {
296 let elem: Element = r#"
297<query xmlns='http://jabber.org/protocol/disco#info'
298 node='http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w='>
299 <identity xml:lang='en' category='client' name='Psi 0.11' type='pc'/>
300 <identity xml:lang='el' category='client' name='Ψ 0.11' type='pc'/>
301 <feature var='http://jabber.org/protocol/caps'/>
302 <feature var='http://jabber.org/protocol/disco#info'/>
303 <feature var='http://jabber.org/protocol/disco#items'/>
304 <feature var='http://jabber.org/protocol/muc'/>
305 <x xmlns='jabber:x:data' type='result'>
306 <field var='FORM_TYPE' type='hidden'>
307 <value>urn:xmpp:dataforms:softwareinfo</value>
308 </field>
309 <field var='ip_version'>
310 <value>ipv4</value>
311 <value>ipv6</value>
312 </field>
313 <field var='os'>
314 <value>Mac</value>
315 </field>
316 <field var='os_version'>
317 <value>10.5.1</value>
318 </field>
319 <field var='software'>
320 <value>Psi</value>
321 </field>
322 <field var='software_version'>
323 <value>0.11</value>
324 </field>
325 </x>
326</query>
327"#
328 .parse()
329 .unwrap();
330 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();
331 let disco = DiscoInfoResult::try_from(elem).unwrap();
332 let caps = caps::compute_disco(&disco);
333 assert_eq!(caps, expected);
334
335 let sha_1 = caps::hash_caps(&caps, Algo::Sha_1).unwrap();
336 assert_eq!(
337 sha_1.hash,
338 base64::decode("q07IKJEyjvHSyhy//CH0CxmKi8w=").unwrap()
339 );
340 }
341}