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