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