1 | /* Copyright 2016 OpenMarket Ltd |
2 | * |
3 | * Licensed under the Apache License, Version 2.0 (the "License"); |
4 | * you may not use this file except in compliance with the License. |
5 | * You may obtain a copy of the License at |
6 | * |
7 | * http://www.apache.org/licenses/LICENSE-2.0 |
8 | * |
9 | * Unless required by applicable law or agreed to in writing, software |
10 | * distributed under the License is distributed on an "AS IS" BASIS, |
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | * See the License for the specific language governing permissions and |
13 | * limitations under the License. |
14 | */ |
15 | |
16 | #include "olm/inbound_group_session.h" |
17 | |
18 | #include <string.h> |
19 | |
20 | #include "olm/base64.h" |
21 | #include "olm/cipher.h" |
22 | #include "olm/crypto.h" |
23 | #include "olm/error.h" |
24 | #include "olm/megolm.h" |
25 | #include "olm/memory.h" |
26 | #include "olm/message.h" |
27 | #include "olm/pickle.h" |
28 | #include "olm/pickle_encoding.h" |
29 | |
30 | |
31 | #define OLM_PROTOCOL_VERSION 3 |
32 | #define GROUP_SESSION_ID_LENGTH ED25519_PUBLIC_KEY_LENGTH |
33 | #define PICKLE_VERSION 2 |
34 | #define SESSION_KEY_VERSION 2 |
35 | #define SESSION_EXPORT_VERSION 1 |
36 | |
37 | struct OlmInboundGroupSession { |
38 | /** our earliest known ratchet value */ |
39 | Megolm initial_ratchet; |
40 | |
41 | /** The most recent ratchet value */ |
42 | Megolm latest_ratchet; |
43 | |
44 | /** The ed25519 signing key */ |
45 | struct _olm_ed25519_public_key signing_key; |
46 | |
47 | /** |
48 | * Have we ever seen any evidence that this is a valid session? |
49 | * (either because the original session share was signed, or because we |
50 | * have subsequently successfully decrypted a message) |
51 | * |
52 | * (We don't do anything with this currently, but we may want to bear it in |
53 | * mind when we consider handling key-shares for sessions we already know |
54 | * about.) |
55 | */ |
56 | int signing_key_verified; |
57 | |
58 | enum OlmErrorCode last_error; |
59 | }; |
60 | |
61 | size_t olm_inbound_group_session_size(void) { |
62 | return sizeof(OlmInboundGroupSession); |
63 | } |
64 | |
65 | OlmInboundGroupSession * olm_inbound_group_session( |
66 | void *memory |
67 | ) { |
68 | OlmInboundGroupSession *session = memory; |
69 | olm_clear_inbound_group_session(session); |
70 | return session; |
71 | } |
72 | |
73 | const char *olm_inbound_group_session_last_error( |
74 | const OlmInboundGroupSession *session |
75 | ) { |
76 | return _olm_error_to_string(error: session->last_error); |
77 | } |
78 | |
79 | enum OlmErrorCode olm_inbound_group_session_last_error_code( |
80 | const OlmInboundGroupSession *session |
81 | ) { |
82 | return session->last_error; |
83 | } |
84 | |
85 | size_t olm_clear_inbound_group_session( |
86 | OlmInboundGroupSession *session |
87 | ) { |
88 | _olm_unset(buffer: session, buffer_length: sizeof(OlmInboundGroupSession)); |
89 | return sizeof(OlmInboundGroupSession); |
90 | } |
91 | |
92 | #define SESSION_EXPORT_RAW_LENGTH \ |
93 | (1 + 4 + MEGOLM_RATCHET_LENGTH + ED25519_PUBLIC_KEY_LENGTH) |
94 | |
95 | #define SESSION_KEY_RAW_LENGTH \ |
96 | (1 + 4 + MEGOLM_RATCHET_LENGTH + ED25519_PUBLIC_KEY_LENGTH\ |
97 | + ED25519_SIGNATURE_LENGTH) |
98 | |
99 | static size_t _init_group_session_keys( |
100 | OlmInboundGroupSession *session, |
101 | const uint8_t *key_buf, |
102 | int export_format |
103 | ) { |
104 | const uint8_t expected_version = |
105 | (export_format ? SESSION_EXPORT_VERSION : SESSION_KEY_VERSION); |
106 | const uint8_t *ptr = key_buf; |
107 | size_t version = *ptr++; |
108 | |
109 | if (version != expected_version) { |
110 | session->last_error = OLM_BAD_SESSION_KEY; |
111 | return (size_t)-1; |
112 | } |
113 | |
114 | uint32_t counter = 0; |
115 | // Decode counter as a big endian 32-bit number. |
116 | for (unsigned i = 0; i < 4; i++) { |
117 | counter <<= 8; counter |= *ptr++; |
118 | } |
119 | |
120 | megolm_init(megolm: &session->initial_ratchet, random_data: ptr, counter); |
121 | megolm_init(megolm: &session->latest_ratchet, random_data: ptr, counter); |
122 | |
123 | ptr += MEGOLM_RATCHET_LENGTH; |
124 | memcpy( |
125 | dest: session->signing_key.public_key, src: ptr, ED25519_PUBLIC_KEY_LENGTH |
126 | ); |
127 | ptr += ED25519_PUBLIC_KEY_LENGTH; |
128 | |
129 | if (!export_format) { |
130 | if (!_olm_crypto_ed25519_verify(their_key: &session->signing_key, message: key_buf, |
131 | message_length: ptr - key_buf, signature: ptr)) { |
132 | session->last_error = OLM_BAD_SIGNATURE; |
133 | return (size_t)-1; |
134 | } |
135 | |
136 | /* signed keyshare */ |
137 | session->signing_key_verified = 1; |
138 | } |
139 | return 0; |
140 | } |
141 | |
142 | size_t olm_init_inbound_group_session( |
143 | OlmInboundGroupSession *session, |
144 | const uint8_t * session_key, size_t session_key_length |
145 | ) { |
146 | uint8_t key_buf[SESSION_KEY_RAW_LENGTH]; |
147 | size_t raw_length = _olm_decode_base64_length(input_length: session_key_length); |
148 | size_t result; |
149 | |
150 | if (raw_length == (size_t)-1) { |
151 | session->last_error = OLM_INVALID_BASE64; |
152 | return (size_t)-1; |
153 | } |
154 | |
155 | if (raw_length != SESSION_KEY_RAW_LENGTH) { |
156 | session->last_error = OLM_BAD_SESSION_KEY; |
157 | return (size_t)-1; |
158 | } |
159 | |
160 | _olm_decode_base64(input: session_key, input_length: session_key_length, output: key_buf); |
161 | result = _init_group_session_keys(session, key_buf, export_format: 0); |
162 | _olm_unset(buffer: key_buf, SESSION_KEY_RAW_LENGTH); |
163 | return result; |
164 | } |
165 | |
166 | size_t olm_import_inbound_group_session( |
167 | OlmInboundGroupSession *session, |
168 | const uint8_t * session_key, size_t session_key_length |
169 | ) { |
170 | uint8_t key_buf[SESSION_EXPORT_RAW_LENGTH]; |
171 | size_t raw_length = _olm_decode_base64_length(input_length: session_key_length); |
172 | size_t result; |
173 | |
174 | if (raw_length == (size_t)-1) { |
175 | session->last_error = OLM_INVALID_BASE64; |
176 | return (size_t)-1; |
177 | } |
178 | |
179 | if (raw_length != SESSION_EXPORT_RAW_LENGTH) { |
180 | session->last_error = OLM_BAD_SESSION_KEY; |
181 | return (size_t)-1; |
182 | } |
183 | |
184 | _olm_decode_base64(input: session_key, input_length: session_key_length, output: key_buf); |
185 | result = _init_group_session_keys(session, key_buf, export_format: 1); |
186 | _olm_unset(buffer: key_buf, SESSION_EXPORT_RAW_LENGTH); |
187 | return result; |
188 | } |
189 | |
190 | static size_t raw_pickle_length( |
191 | const OlmInboundGroupSession *session |
192 | ) { |
193 | size_t length = 0; |
194 | length += _olm_pickle_uint32_length(PICKLE_VERSION); |
195 | length += megolm_pickle_length(megolm: &session->initial_ratchet); |
196 | length += megolm_pickle_length(megolm: &session->latest_ratchet); |
197 | length += _olm_pickle_ed25519_public_key_length(value: &session->signing_key); |
198 | length += _olm_pickle_bool_length(session->signing_key_verified); |
199 | return length; |
200 | } |
201 | |
202 | size_t olm_pickle_inbound_group_session_length( |
203 | const OlmInboundGroupSession *session |
204 | ) { |
205 | return _olm_enc_output_length(raw_length: raw_pickle_length(session)); |
206 | } |
207 | |
208 | size_t olm_pickle_inbound_group_session( |
209 | OlmInboundGroupSession *session, |
210 | void const * key, size_t key_length, |
211 | void * pickled, size_t pickled_length |
212 | ) { |
213 | size_t raw_length = raw_pickle_length(session); |
214 | uint8_t *pos; |
215 | |
216 | if (pickled_length < _olm_enc_output_length(raw_length)) { |
217 | session->last_error = OLM_OUTPUT_BUFFER_TOO_SMALL; |
218 | return (size_t)-1; |
219 | } |
220 | |
221 | pos = _olm_enc_output_pos(output: pickled, raw_length); |
222 | pos = _olm_pickle_uint32(pos, PICKLE_VERSION); |
223 | pos = megolm_pickle(megolm: &session->initial_ratchet, pos); |
224 | pos = megolm_pickle(megolm: &session->latest_ratchet, pos); |
225 | pos = _olm_pickle_ed25519_public_key(pos, value: &session->signing_key); |
226 | pos = _olm_pickle_bool(pos, value: session->signing_key_verified); |
227 | |
228 | return _olm_enc_output(key, key_length, pickle: pickled, raw_length); |
229 | } |
230 | |
231 | size_t olm_unpickle_inbound_group_session( |
232 | OlmInboundGroupSession *session, |
233 | void const * key, size_t key_length, |
234 | void * pickled, size_t pickled_length |
235 | ) { |
236 | const uint8_t *pos; |
237 | const uint8_t *end; |
238 | uint32_t pickle_version; |
239 | |
240 | size_t raw_length = _olm_enc_input( |
241 | key, key_length, input: pickled, b64_length: pickled_length, last_error: &(session->last_error) |
242 | ); |
243 | if (raw_length == (size_t)-1) { |
244 | return raw_length; |
245 | } |
246 | |
247 | pos = pickled; |
248 | end = pos + raw_length; |
249 | |
250 | pos = _olm_unpickle_uint32(pos, end, value: &pickle_version); |
251 | FAIL_ON_CORRUPTED_PICKLE(pos, session); |
252 | |
253 | if (pickle_version < 1 || pickle_version > PICKLE_VERSION) { |
254 | session->last_error = OLM_UNKNOWN_PICKLE_VERSION; |
255 | return (size_t)-1; |
256 | } |
257 | |
258 | pos = megolm_unpickle(megolm: &session->initial_ratchet, pos, end); |
259 | FAIL_ON_CORRUPTED_PICKLE(pos, session); |
260 | |
261 | pos = megolm_unpickle(megolm: &session->latest_ratchet, pos, end); |
262 | FAIL_ON_CORRUPTED_PICKLE(pos, session); |
263 | |
264 | pos = _olm_unpickle_ed25519_public_key(pos, end, value: &session->signing_key); |
265 | FAIL_ON_CORRUPTED_PICKLE(pos, session); |
266 | |
267 | if (pickle_version == 1) { |
268 | /* pickle v1 had no signing_key_verified field (all keyshares were |
269 | * verified at import time) */ |
270 | session->signing_key_verified = 1; |
271 | } else { |
272 | pos = _olm_unpickle_bool(pos, end, value: &(session->signing_key_verified)); |
273 | } |
274 | FAIL_ON_CORRUPTED_PICKLE(pos, session); |
275 | |
276 | if (pos != end) { |
277 | /* Input was longer than expected. */ |
278 | session->last_error = OLM_PICKLE_EXTRA_DATA; |
279 | return (size_t)-1; |
280 | } |
281 | |
282 | return pickled_length; |
283 | } |
284 | |
285 | /** |
286 | * get the max plaintext length in an un-base64-ed message |
287 | */ |
288 | static size_t _decrypt_max_plaintext_length( |
289 | OlmInboundGroupSession *session, |
290 | uint8_t * message, size_t message_length |
291 | ) { |
292 | struct _OlmDecodeGroupMessageResults decoded_results; |
293 | |
294 | _olm_decode_group_message( |
295 | input: message, input_length: message_length, |
296 | mac_length: megolm_cipher->ops->mac_length(megolm_cipher), |
297 | ED25519_SIGNATURE_LENGTH, |
298 | results: &decoded_results); |
299 | |
300 | if (decoded_results.version != OLM_PROTOCOL_VERSION) { |
301 | session->last_error = OLM_BAD_MESSAGE_VERSION; |
302 | return (size_t)-1; |
303 | } |
304 | |
305 | if (!decoded_results.ciphertext) { |
306 | session->last_error = OLM_BAD_MESSAGE_FORMAT; |
307 | return (size_t)-1; |
308 | } |
309 | |
310 | return megolm_cipher->ops->decrypt_max_plaintext_length( |
311 | megolm_cipher, decoded_results.ciphertext_length); |
312 | } |
313 | |
314 | size_t olm_group_decrypt_max_plaintext_length( |
315 | OlmInboundGroupSession *session, |
316 | uint8_t * message, size_t message_length |
317 | ) { |
318 | size_t raw_length; |
319 | |
320 | raw_length = _olm_decode_base64(input: message, input_length: message_length, output: message); |
321 | if (raw_length == (size_t)-1) { |
322 | session->last_error = OLM_INVALID_BASE64; |
323 | return (size_t)-1; |
324 | } |
325 | |
326 | return _decrypt_max_plaintext_length( |
327 | session, message, message_length: raw_length |
328 | ); |
329 | } |
330 | |
331 | /** |
332 | * get a copy of the megolm ratchet, advanced |
333 | * to the relevant index. Returns 0 on success, -1 on error |
334 | */ |
335 | static size_t _get_megolm( |
336 | OlmInboundGroupSession *session, uint32_t message_index, Megolm *result |
337 | ) { |
338 | /* pick a megolm instance to use. If we're at or beyond the latest ratchet |
339 | * value, use that */ |
340 | if ((message_index - session->latest_ratchet.counter) < (1U << 31)) { |
341 | megolm_advance_to(megolm: &session->latest_ratchet, advance_to: message_index); |
342 | *result = session->latest_ratchet; |
343 | return 0; |
344 | } else if ((message_index - session->initial_ratchet.counter) >= (1U << 31)) { |
345 | /* the counter is before our intial ratchet - we can't decode this. */ |
346 | session->last_error = OLM_UNKNOWN_MESSAGE_INDEX; |
347 | return (size_t)-1; |
348 | } else { |
349 | /* otherwise, start from the initial megolm. Take a copy so that we |
350 | * don't overwrite the initial megolm */ |
351 | *result = session->initial_ratchet; |
352 | megolm_advance_to(megolm: result, advance_to: message_index); |
353 | return 0; |
354 | } |
355 | } |
356 | |
357 | /** |
358 | * decrypt an un-base64-ed message |
359 | */ |
360 | static size_t _decrypt( |
361 | OlmInboundGroupSession *session, |
362 | uint8_t * message, size_t message_length, |
363 | uint8_t * plaintext, size_t max_plaintext_length, |
364 | uint32_t * message_index |
365 | ) { |
366 | struct _OlmDecodeGroupMessageResults decoded_results; |
367 | size_t max_length, r; |
368 | Megolm megolm; |
369 | |
370 | _olm_decode_group_message( |
371 | input: message, input_length: message_length, |
372 | mac_length: megolm_cipher->ops->mac_length(megolm_cipher), |
373 | ED25519_SIGNATURE_LENGTH, |
374 | results: &decoded_results); |
375 | |
376 | if (decoded_results.version != OLM_PROTOCOL_VERSION) { |
377 | session->last_error = OLM_BAD_MESSAGE_VERSION; |
378 | return (size_t)-1; |
379 | } |
380 | |
381 | if (!decoded_results.has_message_index || !decoded_results.ciphertext) { |
382 | session->last_error = OLM_BAD_MESSAGE_FORMAT; |
383 | return (size_t)-1; |
384 | } |
385 | |
386 | if (message_index != NULL) { |
387 | *message_index = decoded_results.message_index; |
388 | } |
389 | |
390 | /* verify the signature. We could do this before decoding the message, but |
391 | * we allow for the possibility of future protocol versions which use a |
392 | * different signing mechanism; we would rather throw "BAD_MESSAGE_VERSION" |
393 | * than "BAD_SIGNATURE" in this case. |
394 | */ |
395 | message_length -= ED25519_SIGNATURE_LENGTH; |
396 | r = _olm_crypto_ed25519_verify( |
397 | their_key: &session->signing_key, |
398 | message, message_length, |
399 | signature: message + message_length |
400 | ); |
401 | if (!r) { |
402 | session->last_error = OLM_BAD_SIGNATURE; |
403 | return (size_t)-1; |
404 | } |
405 | |
406 | max_length = megolm_cipher->ops->decrypt_max_plaintext_length( |
407 | megolm_cipher, |
408 | decoded_results.ciphertext_length |
409 | ); |
410 | if (max_plaintext_length < max_length) { |
411 | session->last_error = OLM_OUTPUT_BUFFER_TOO_SMALL; |
412 | return (size_t)-1; |
413 | } |
414 | |
415 | r = _get_megolm(session, message_index: decoded_results.message_index, result: &megolm); |
416 | if (r == (size_t)-1) { |
417 | return r; |
418 | } |
419 | |
420 | /* now try checking the mac, and decrypting */ |
421 | r = megolm_cipher->ops->decrypt( |
422 | megolm_cipher, |
423 | megolm_get_data(&megolm), MEGOLM_RATCHET_LENGTH, |
424 | message, message_length, |
425 | decoded_results.ciphertext, decoded_results.ciphertext_length, |
426 | plaintext, max_plaintext_length |
427 | ); |
428 | |
429 | _olm_unset(buffer: &megolm, buffer_length: sizeof(megolm)); |
430 | if (r == (size_t)-1) { |
431 | session->last_error = OLM_BAD_MESSAGE_MAC; |
432 | return r; |
433 | } |
434 | |
435 | /* once we have successfully decrypted a message, set a flag to say the |
436 | * session appears valid. */ |
437 | session->signing_key_verified = 1; |
438 | |
439 | return r; |
440 | } |
441 | |
442 | size_t olm_group_decrypt( |
443 | OlmInboundGroupSession *session, |
444 | uint8_t * message, size_t message_length, |
445 | uint8_t * plaintext, size_t max_plaintext_length, |
446 | uint32_t * message_index |
447 | ) { |
448 | size_t raw_message_length; |
449 | |
450 | raw_message_length = _olm_decode_base64(input: message, input_length: message_length, output: message); |
451 | if (raw_message_length == (size_t)-1) { |
452 | session->last_error = OLM_INVALID_BASE64; |
453 | return (size_t)-1; |
454 | } |
455 | |
456 | return _decrypt( |
457 | session, message, message_length: raw_message_length, |
458 | plaintext, max_plaintext_length, |
459 | message_index |
460 | ); |
461 | } |
462 | |
463 | size_t olm_inbound_group_session_id_length( |
464 | const OlmInboundGroupSession *session |
465 | ) { |
466 | return _olm_encode_base64_length(GROUP_SESSION_ID_LENGTH); |
467 | } |
468 | |
469 | size_t olm_inbound_group_session_id( |
470 | OlmInboundGroupSession *session, |
471 | uint8_t * id, size_t id_length |
472 | ) { |
473 | if (id_length < olm_inbound_group_session_id_length(session)) { |
474 | session->last_error = OLM_OUTPUT_BUFFER_TOO_SMALL; |
475 | return (size_t)-1; |
476 | } |
477 | |
478 | return _olm_encode_base64( |
479 | input: session->signing_key.public_key, GROUP_SESSION_ID_LENGTH, output: id |
480 | ); |
481 | } |
482 | |
483 | uint32_t olm_inbound_group_session_first_known_index( |
484 | const OlmInboundGroupSession *session |
485 | ) { |
486 | return session->initial_ratchet.counter; |
487 | } |
488 | |
489 | int olm_inbound_group_session_is_verified( |
490 | const OlmInboundGroupSession *session |
491 | ) { |
492 | return session->signing_key_verified; |
493 | } |
494 | |
495 | size_t olm_export_inbound_group_session_length( |
496 | const OlmInboundGroupSession *session |
497 | ) { |
498 | return _olm_encode_base64_length(SESSION_EXPORT_RAW_LENGTH); |
499 | } |
500 | |
501 | size_t olm_export_inbound_group_session( |
502 | OlmInboundGroupSession *session, |
503 | uint8_t * key, size_t key_length, uint32_t message_index |
504 | ) { |
505 | uint8_t *raw; |
506 | uint8_t *ptr; |
507 | Megolm megolm; |
508 | size_t r; |
509 | size_t encoded_length = olm_export_inbound_group_session_length(session); |
510 | |
511 | if (key_length < encoded_length) { |
512 | session->last_error = OLM_OUTPUT_BUFFER_TOO_SMALL; |
513 | return (size_t)-1; |
514 | } |
515 | |
516 | r = _get_megolm(session, message_index, result: &megolm); |
517 | if (r == (size_t)-1) { |
518 | return r; |
519 | } |
520 | |
521 | /* put the raw data at the end of the output buffer. */ |
522 | raw = ptr = key + encoded_length - SESSION_EXPORT_RAW_LENGTH; |
523 | *ptr++ = SESSION_EXPORT_VERSION; |
524 | |
525 | // Encode message index as a big endian 32-bit number. |
526 | for (unsigned i = 0; i < 4; i++) { |
527 | *ptr++ = 0xFF & (message_index >> 24); message_index <<= 8; |
528 | } |
529 | |
530 | memcpy(dest: ptr, megolm_get_data(&megolm), MEGOLM_RATCHET_LENGTH); |
531 | ptr += MEGOLM_RATCHET_LENGTH; |
532 | |
533 | memcpy( |
534 | dest: ptr, src: session->signing_key.public_key, |
535 | ED25519_PUBLIC_KEY_LENGTH |
536 | ); |
537 | ptr += ED25519_PUBLIC_KEY_LENGTH; |
538 | |
539 | return _olm_encode_base64(input: raw, SESSION_EXPORT_RAW_LENGTH, output: key); |
540 | } |
541 | |