| 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/outbound_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 | #define OLM_PROTOCOL_VERSION 3 |
| 31 | #define GROUP_SESSION_ID_LENGTH ED25519_PUBLIC_KEY_LENGTH |
| 32 | #define PICKLE_VERSION 1 |
| 33 | #define SESSION_KEY_VERSION 2 |
| 34 | |
| 35 | struct OlmOutboundGroupSession { |
| 36 | /** the Megolm ratchet providing the encryption keys */ |
| 37 | Megolm ratchet; |
| 38 | |
| 39 | /** The ed25519 keypair used for signing the messages */ |
| 40 | struct _olm_ed25519_key_pair signing_key; |
| 41 | |
| 42 | enum OlmErrorCode last_error; |
| 43 | }; |
| 44 | |
| 45 | |
| 46 | size_t olm_outbound_group_session_size(void) { |
| 47 | return sizeof(OlmOutboundGroupSession); |
| 48 | } |
| 49 | |
| 50 | OlmOutboundGroupSession * olm_outbound_group_session( |
| 51 | void *memory |
| 52 | ) { |
| 53 | OlmOutboundGroupSession *session = memory; |
| 54 | olm_clear_outbound_group_session(session); |
| 55 | return session; |
| 56 | } |
| 57 | |
| 58 | const char *olm_outbound_group_session_last_error( |
| 59 | const OlmOutboundGroupSession *session |
| 60 | ) { |
| 61 | return _olm_error_to_string(error: session->last_error); |
| 62 | } |
| 63 | |
| 64 | enum OlmErrorCode olm_outbound_group_session_last_error_code( |
| 65 | const OlmOutboundGroupSession *session |
| 66 | ) { |
| 67 | return session->last_error; |
| 68 | } |
| 69 | |
| 70 | size_t olm_clear_outbound_group_session( |
| 71 | OlmOutboundGroupSession *session |
| 72 | ) { |
| 73 | _olm_unset(buffer: session, buffer_length: sizeof(OlmOutboundGroupSession)); |
| 74 | return sizeof(OlmOutboundGroupSession); |
| 75 | } |
| 76 | |
| 77 | static size_t raw_pickle_length( |
| 78 | const OlmOutboundGroupSession *session |
| 79 | ) { |
| 80 | size_t length = 0; |
| 81 | length += _olm_pickle_uint32_length(PICKLE_VERSION); |
| 82 | length += megolm_pickle_length(megolm: &(session->ratchet)); |
| 83 | length += _olm_pickle_ed25519_key_pair_length(value: &(session->signing_key)); |
| 84 | return length; |
| 85 | } |
| 86 | |
| 87 | size_t olm_pickle_outbound_group_session_length( |
| 88 | const OlmOutboundGroupSession *session |
| 89 | ) { |
| 90 | return _olm_enc_output_length(raw_length: raw_pickle_length(session)); |
| 91 | } |
| 92 | |
| 93 | size_t olm_pickle_outbound_group_session( |
| 94 | OlmOutboundGroupSession *session, |
| 95 | void const * key, size_t key_length, |
| 96 | void * pickled, size_t pickled_length |
| 97 | ) { |
| 98 | size_t raw_length = raw_pickle_length(session); |
| 99 | uint8_t *pos; |
| 100 | |
| 101 | if (pickled_length < _olm_enc_output_length(raw_length)) { |
| 102 | session->last_error = OLM_OUTPUT_BUFFER_TOO_SMALL; |
| 103 | return (size_t)-1; |
| 104 | } |
| 105 | |
| 106 | #ifndef OLM_FUZZING |
| 107 | pos = _olm_enc_output_pos(output: pickled, raw_length); |
| 108 | #else |
| 109 | pos = pickled; |
| 110 | #endif |
| 111 | |
| 112 | pos = _olm_pickle_uint32(pos, PICKLE_VERSION); |
| 113 | pos = megolm_pickle(megolm: &(session->ratchet), pos); |
| 114 | pos = _olm_pickle_ed25519_key_pair(pos, value: &(session->signing_key)); |
| 115 | |
| 116 | #ifndef OLM_FUZZING |
| 117 | return _olm_enc_output(key, key_length, pickle: pickled, raw_length); |
| 118 | #else |
| 119 | return raw_length; |
| 120 | #endif |
| 121 | } |
| 122 | |
| 123 | size_t olm_unpickle_outbound_group_session( |
| 124 | OlmOutboundGroupSession *session, |
| 125 | void const * key, size_t key_length, |
| 126 | void * pickled, size_t pickled_length |
| 127 | ) { |
| 128 | const uint8_t *pos; |
| 129 | const uint8_t *end; |
| 130 | uint32_t pickle_version; |
| 131 | |
| 132 | #ifndef OLM_FUZZING |
| 133 | size_t raw_length = _olm_enc_input( |
| 134 | key, key_length, input: pickled, b64_length: pickled_length, last_error: &(session->last_error) |
| 135 | ); |
| 136 | #else |
| 137 | size_t raw_length = pickled_length; |
| 138 | #endif |
| 139 | |
| 140 | if (raw_length == (size_t)-1) { |
| 141 | return raw_length; |
| 142 | } |
| 143 | |
| 144 | pos = pickled; |
| 145 | end = pos + raw_length; |
| 146 | |
| 147 | pos = _olm_unpickle_uint32(pos, end, value: &pickle_version); |
| 148 | FAIL_ON_CORRUPTED_PICKLE(pos, session); |
| 149 | |
| 150 | if (pickle_version != PICKLE_VERSION) { |
| 151 | session->last_error = OLM_UNKNOWN_PICKLE_VERSION; |
| 152 | return (size_t)-1; |
| 153 | } |
| 154 | |
| 155 | pos = megolm_unpickle(megolm: &(session->ratchet), pos, end); |
| 156 | FAIL_ON_CORRUPTED_PICKLE(pos, session); |
| 157 | |
| 158 | pos = _olm_unpickle_ed25519_key_pair(pos, end, value: &(session->signing_key)); |
| 159 | FAIL_ON_CORRUPTED_PICKLE(pos, session); |
| 160 | |
| 161 | if (pos != end) { |
| 162 | /* Input was longer than expected. */ |
| 163 | session->last_error = OLM_PICKLE_EXTRA_DATA; |
| 164 | return (size_t)-1; |
| 165 | } |
| 166 | |
| 167 | return pickled_length; |
| 168 | } |
| 169 | |
| 170 | |
| 171 | size_t olm_init_outbound_group_session_random_length( |
| 172 | const OlmOutboundGroupSession *session |
| 173 | ) { |
| 174 | /* we need data to initialize the megolm ratchet, plus some more for the |
| 175 | * session id. |
| 176 | */ |
| 177 | return MEGOLM_RATCHET_LENGTH + |
| 178 | ED25519_RANDOM_LENGTH; |
| 179 | } |
| 180 | |
| 181 | size_t olm_init_outbound_group_session( |
| 182 | OlmOutboundGroupSession *session, |
| 183 | uint8_t *random, size_t random_length |
| 184 | ) { |
| 185 | const uint8_t *random_ptr = random; |
| 186 | |
| 187 | if (random_length < olm_init_outbound_group_session_random_length(session)) { |
| 188 | /* Insufficient random data for new session */ |
| 189 | session->last_error = OLM_NOT_ENOUGH_RANDOM; |
| 190 | return (size_t)-1; |
| 191 | } |
| 192 | |
| 193 | megolm_init(megolm: &(session->ratchet), random_data: random_ptr, counter: 0); |
| 194 | random_ptr += MEGOLM_RATCHET_LENGTH; |
| 195 | |
| 196 | _olm_crypto_ed25519_generate_key(random_bytes: random_ptr, output: &(session->signing_key)); |
| 197 | random_ptr += ED25519_RANDOM_LENGTH; |
| 198 | |
| 199 | _olm_unset(buffer: random, buffer_length: random_length); |
| 200 | return 0; |
| 201 | } |
| 202 | |
| 203 | static size_t raw_message_length( |
| 204 | OlmOutboundGroupSession *session, |
| 205 | size_t plaintext_length) |
| 206 | { |
| 207 | size_t ciphertext_length, mac_length; |
| 208 | |
| 209 | ciphertext_length = megolm_cipher->ops->encrypt_ciphertext_length( |
| 210 | megolm_cipher, plaintext_length |
| 211 | ); |
| 212 | |
| 213 | mac_length = megolm_cipher->ops->mac_length(megolm_cipher); |
| 214 | |
| 215 | return _olm_encode_group_message_length( |
| 216 | chain_index: session->ratchet.counter, |
| 217 | ciphertext_length, mac_length, ED25519_SIGNATURE_LENGTH |
| 218 | ); |
| 219 | } |
| 220 | |
| 221 | size_t olm_group_encrypt_message_length( |
| 222 | OlmOutboundGroupSession *session, |
| 223 | size_t plaintext_length |
| 224 | ) { |
| 225 | size_t message_length = raw_message_length(session, plaintext_length); |
| 226 | return _olm_encode_base64_length(input_length: message_length); |
| 227 | } |
| 228 | |
| 229 | /** write an un-base64-ed message to the buffer */ |
| 230 | static size_t _encrypt( |
| 231 | OlmOutboundGroupSession *session, uint8_t const * plaintext, size_t plaintext_length, |
| 232 | uint8_t * buffer |
| 233 | ) { |
| 234 | size_t ciphertext_length, mac_length, message_length; |
| 235 | size_t result; |
| 236 | uint8_t *ciphertext_ptr; |
| 237 | |
| 238 | ciphertext_length = megolm_cipher->ops->encrypt_ciphertext_length( |
| 239 | megolm_cipher, |
| 240 | plaintext_length |
| 241 | ); |
| 242 | |
| 243 | mac_length = megolm_cipher->ops->mac_length(megolm_cipher); |
| 244 | |
| 245 | /* first we build the message structure, then we encrypt |
| 246 | * the plaintext into it. |
| 247 | */ |
| 248 | message_length = _olm_encode_group_message( |
| 249 | OLM_PROTOCOL_VERSION, |
| 250 | message_index: session->ratchet.counter, |
| 251 | ciphertext_length, |
| 252 | output: buffer, |
| 253 | ciphertext_ptr: &ciphertext_ptr); |
| 254 | |
| 255 | message_length += mac_length; |
| 256 | |
| 257 | result = megolm_cipher->ops->encrypt( |
| 258 | megolm_cipher, |
| 259 | megolm_get_data(&(session->ratchet)), MEGOLM_RATCHET_LENGTH, |
| 260 | plaintext, plaintext_length, |
| 261 | ciphertext_ptr, ciphertext_length, |
| 262 | buffer, message_length |
| 263 | ); |
| 264 | |
| 265 | if (result == (size_t)-1) { |
| 266 | return result; |
| 267 | } |
| 268 | |
| 269 | megolm_advance(megolm: &(session->ratchet)); |
| 270 | |
| 271 | /* sign the whole thing with the ed25519 key. */ |
| 272 | _olm_crypto_ed25519_sign( |
| 273 | our_key: &(session->signing_key), |
| 274 | message: buffer, message_length, |
| 275 | output: buffer + message_length |
| 276 | ); |
| 277 | |
| 278 | return result; |
| 279 | } |
| 280 | |
| 281 | size_t olm_group_encrypt( |
| 282 | OlmOutboundGroupSession *session, |
| 283 | uint8_t const * plaintext, size_t plaintext_length, |
| 284 | uint8_t * message, size_t max_message_length |
| 285 | ) { |
| 286 | size_t rawmsglen; |
| 287 | size_t result; |
| 288 | uint8_t *message_pos; |
| 289 | |
| 290 | rawmsglen = raw_message_length(session, plaintext_length); |
| 291 | |
| 292 | if (max_message_length < _olm_encode_base64_length(input_length: rawmsglen)) { |
| 293 | session->last_error = OLM_OUTPUT_BUFFER_TOO_SMALL; |
| 294 | return (size_t)-1; |
| 295 | } |
| 296 | |
| 297 | /* we construct the message at the end of the buffer, so that |
| 298 | * we have room to base64-encode it once we're done. |
| 299 | */ |
| 300 | message_pos = message + _olm_encode_base64_length(input_length: rawmsglen) - rawmsglen; |
| 301 | |
| 302 | /* write the message, and encrypt it, at message_pos */ |
| 303 | result = _encrypt(session, plaintext, plaintext_length, buffer: message_pos); |
| 304 | if (result == (size_t)-1) { |
| 305 | return result; |
| 306 | } |
| 307 | |
| 308 | /* bas64-encode it */ |
| 309 | return _olm_encode_base64( |
| 310 | input: message_pos, input_length: rawmsglen, output: message |
| 311 | ); |
| 312 | } |
| 313 | |
| 314 | |
| 315 | size_t olm_outbound_group_session_id_length( |
| 316 | const OlmOutboundGroupSession *session |
| 317 | ) { |
| 318 | return _olm_encode_base64_length(GROUP_SESSION_ID_LENGTH); |
| 319 | } |
| 320 | |
| 321 | size_t olm_outbound_group_session_id( |
| 322 | OlmOutboundGroupSession *session, |
| 323 | uint8_t * id, size_t id_length |
| 324 | ) { |
| 325 | if (id_length < olm_outbound_group_session_id_length(session)) { |
| 326 | session->last_error = OLM_OUTPUT_BUFFER_TOO_SMALL; |
| 327 | return (size_t)-1; |
| 328 | } |
| 329 | |
| 330 | return _olm_encode_base64( |
| 331 | input: session->signing_key.public_key.public_key, GROUP_SESSION_ID_LENGTH, output: id |
| 332 | ); |
| 333 | } |
| 334 | |
| 335 | uint32_t olm_outbound_group_session_message_index( |
| 336 | OlmOutboundGroupSession *session |
| 337 | ) { |
| 338 | return session->ratchet.counter; |
| 339 | } |
| 340 | |
| 341 | #define SESSION_KEY_RAW_LENGTH \ |
| 342 | (1 + 4 + MEGOLM_RATCHET_LENGTH + ED25519_PUBLIC_KEY_LENGTH\ |
| 343 | + ED25519_SIGNATURE_LENGTH) |
| 344 | |
| 345 | size_t olm_outbound_group_session_key_length( |
| 346 | const OlmOutboundGroupSession *session |
| 347 | ) { |
| 348 | return _olm_encode_base64_length(SESSION_KEY_RAW_LENGTH); |
| 349 | } |
| 350 | |
| 351 | size_t olm_outbound_group_session_key( |
| 352 | OlmOutboundGroupSession *session, |
| 353 | uint8_t * key, size_t key_length |
| 354 | ) { |
| 355 | uint8_t *raw; |
| 356 | uint8_t *ptr; |
| 357 | size_t encoded_length = olm_outbound_group_session_key_length(session); |
| 358 | |
| 359 | if (key_length < encoded_length) { |
| 360 | session->last_error = OLM_OUTPUT_BUFFER_TOO_SMALL; |
| 361 | return (size_t)-1; |
| 362 | } |
| 363 | |
| 364 | /* put the raw data at the end of the output buffer. */ |
| 365 | raw = ptr = key + encoded_length - SESSION_KEY_RAW_LENGTH; |
| 366 | *ptr++ = SESSION_KEY_VERSION; |
| 367 | |
| 368 | uint32_t counter = session->ratchet.counter; |
| 369 | // Encode counter as a big endian 32-bit number. |
| 370 | for (unsigned i = 0; i < 4; i++) { |
| 371 | *ptr++ = 0xFF & (counter >> 24); counter <<= 8; |
| 372 | } |
| 373 | |
| 374 | memcpy(dest: ptr, megolm_get_data(&session->ratchet), MEGOLM_RATCHET_LENGTH); |
| 375 | ptr += MEGOLM_RATCHET_LENGTH; |
| 376 | |
| 377 | memcpy( |
| 378 | dest: ptr, src: session->signing_key.public_key.public_key, |
| 379 | ED25519_PUBLIC_KEY_LENGTH |
| 380 | ); |
| 381 | ptr += ED25519_PUBLIC_KEY_LENGTH; |
| 382 | |
| 383 | /* sign the whole thing with the ed25519 key. */ |
| 384 | _olm_crypto_ed25519_sign( |
| 385 | our_key: &(session->signing_key), |
| 386 | message: raw, message_length: ptr - raw, output: ptr |
| 387 | ); |
| 388 | |
| 389 | return _olm_encode_base64(input: raw, SESSION_KEY_RAW_LENGTH, output: key); |
| 390 | } |
| 391 | |