Merge branch 'python-sas'

This commit is contained in:
Damir Jelić 2019-04-10 15:18:07 +02:00
commit 086133f39a
8 changed files with 444 additions and 11 deletions

View file

@ -1,4 +1,5 @@
include include/olm/olm.h
include include/olm/pk.h
include include/olm/sas.h
include Makefile
include olm_build.py

View file

@ -12,7 +12,10 @@ include/olm/olm.h: $(OLM_HEADERS)
include/olm/pk.h: include/olm/olm.h ../include/olm/pk.h
$(CPP) -I dummy -I ../include ../include/olm/pk.h -o include/olm/pk.h
headers: include/olm/olm.h include/olm/pk.h
include/olm/sas.h: include/olm/olm.h ../include/olm/sas.h
$(CPP) -I dummy -I ../include ../include/olm/sas.h -o include/olm/sas.h
headers: include/olm/olm.h include/olm/pk.h include/olm/sas.h
olm-python2: headers
DEVELOP=$(DEVELOP) python2 setup.py build

View file

@ -21,7 +21,7 @@ Olm Python bindings
| © Copyright 2015-2017 by OpenMarket Ltd
| © Copyright 2018 by Damir Jelić
"""
from .utility import ed25519_verify, OlmVerifyError
from .utility import ed25519_verify, OlmVerifyError, OlmHashError, sha256
from .account import Account, OlmAccountError
from .session import (
Session,
@ -43,3 +43,4 @@ from .pk import (
PkEncryptionError,
PkDecryptionError
)
from .sas import Sas, OlmSasError

257
python/olm/sas.py Normal file
View file

@ -0,0 +1,257 @@
# -*- coding: utf-8 -*-
# libolm python bindings
# Copyright © 2019 Damir Jelić <poljar@termina.org.uk>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""libolm SAS module.
This module contains functions to perform key verification using the Short
Authentication String (SAS) method.
Examples:
>>> sas = Sas()
>>> bob_key = "3p7bfXt9wbTTW2HC7OQ1Nz+DQ8hbeGdNrfx+FG+IK08"
>>> message = "Hello world!"
>>> extra_info = "MAC"
>>> sas_alice.set_their_pubkey(bob_key)
>>> sas_alice.calculate_mac(message, extra_info)
>>> sas_alice.generate_bytes(extra_info, 5)
"""
from functools import wraps
from builtins import bytes
from typing import Optional
from future.utils import bytes_to_native_str
from _libolm import ffi, lib
from ._compat import URANDOM, to_bytes, to_bytearray
from ._finalize import track_for_finalization
def other_pubkey_set(func):
"""Ensure that the other pubkey is added to the Sas object."""
@wraps(func)
def wrapper(self, *args, **kwargs):
if not self.other_key_set:
raise OlmSasError("The other public key isn't set.")
return func(self, *args, **kwargs)
return wrapper
def _clear_sas(sas):
# type: (ffi.cdata) -> None
lib.olm_clear_sas(sas)
class OlmSasError(Exception):
"""libolm Sas error exception."""
class Sas(object):
"""libolm Short Authenticaton String (SAS) class."""
def __init__(self, other_users_pubkey=None):
# type: (Optional[str]) -> None
"""Create a new SAS object.
Args:
other_users_pubkey(str, optional): The other users public key, this
key is necesary to generate bytes for the authentication string
as well as to calculate the MAC.
Attributes:
other_key_set (bool): A boolean flag that tracks if we set the
other users public key for this SAS object.
Raises OlmSasError on failure.
"""
self._buf = ffi.new("char[]", lib.olm_sas_size())
self._sas = lib.olm_sas(self._buf)
self.other_key_set = False
track_for_finalization(self, self._sas, _clear_sas)
random_length = lib.olm_create_sas_random_length(self._sas)
random = URANDOM(random_length)
self._create_sas(random, random_length)
if other_users_pubkey:
self.set_their_pubkey(other_users_pubkey)
def _create_sas(self, buffer, buffer_length):
self._check_error(
lib.olm_create_sas(
self._sas,
ffi.from_buffer(buffer),
buffer_length
)
)
def _check_error(self, ret):
# type: (int) -> None
if ret != lib.olm_error():
return
last_error = bytes_to_native_str(
ffi.string((lib.olm_sas_last_error(self._sas))))
raise OlmSasError(last_error)
@property
def pubkey(self):
# type: () -> str
"""Get the public key for the SAS object.
This returns the public key of the SAS object that can then be shared
with another user to perform the authentication process.
Raises OlmSasError on failure.
"""
pubkey_length = lib.olm_sas_pubkey_length(self._sas)
pubkey_buffer = ffi.new("char[]", pubkey_length)
self._check_error(
lib.olm_sas_get_pubkey(self._sas, pubkey_buffer, pubkey_length)
)
return bytes_to_native_str(ffi.unpack(pubkey_buffer, pubkey_length))
def set_their_pubkey(self, key):
# type: (str) -> None
"""Set the public key of the other user.
This sets the public key of the other user, it needs to be set before
bytes can be generated for the authentication string and a MAC can be
calculated.
Args:
key (str): The other users public key.
Raises OlmSasError on failure.
"""
byte_key = to_bytearray(key)
self._check_error(
lib.olm_sas_set_their_key(
self._sas,
ffi.from_buffer(byte_key),
len(byte_key)
)
)
self.other_key_set = True
@other_pubkey_set
def generate_bytes(self, extra_info, length):
# type: (str, int) -> bytes
"""Generate bytes to use for the short authentication string.
Args:
extra_info (str): Extra information to mix in when generating the
bytes.
length (int): The number of bytes to generate.
Raises OlmSasError if the other users persons public key isn't set or
an internal Olm error happens.
"""
if length < 1:
raise ValueError("The length needs to be a positive integer value")
byte_info = to_bytearray(extra_info)
out_buffer = ffi.new("char[]", length)
self._check_error(
lib.olm_sas_generate_bytes(
self._sas,
ffi.from_buffer(byte_info),
len(byte_info),
out_buffer,
length
)
)
return ffi.unpack(out_buffer, length)
@other_pubkey_set
def calculate_mac(self, message, extra_info):
# type: (str, str) -> str
"""Generate a message authentication code based on the shared secret.
Args:
message (str): The message to produce the authentication code for.
extra_info (str): Extra information to mix in when generating the
MAC
Raises OlmSasError on failure.
"""
byte_message = to_bytes(message)
byte_info = to_bytes(extra_info)
mac_length = lib.olm_sas_mac_length(self._sas)
mac_buffer = ffi.new("char[]", mac_length)
self._check_error(
lib.olm_sas_calculate_mac(
self._sas,
ffi.from_buffer(byte_message),
len(byte_message),
ffi.from_buffer(byte_info),
len(byte_info),
mac_buffer,
mac_length
)
)
return bytes_to_native_str(ffi.unpack(mac_buffer, mac_length))
@other_pubkey_set
def calculate_mac_long_kdf(self, message, extra_info):
# type: (str, str) -> str
"""Generate a message authentication code based on the shared secret.
This function should not be used unless compatibility with an older
non-tagged Olm version is required.
Args:
message (str): The message to produce the authentication code for.
extra_info (str): Extra information to mix in when generating the
MAC
Raises OlmSasError on failure.
"""
byte_message = to_bytes(message)
byte_info = to_bytes(extra_info)
mac_length = lib.olm_sas_mac_length(self._sas)
mac_buffer = ffi.new("char[]", mac_length)
self._check_error(
lib.olm_sas_calculate_mac_long_kdf(
self._sas,
ffi.from_buffer(byte_message),
len(byte_message),
ffi.from_buffer(byte_info),
len(byte_info),
mac_buffer,
mac_length
)
)
return bytes_to_native_str(ffi.unpack(mac_buffer, mac_length))

View file

@ -32,6 +32,7 @@ Examples:
# pylint: disable=redefined-builtin,unused-import
from typing import AnyStr, Type
from future.utils import bytes_to_native_str
# pylint: disable=no-name-in-module
from _libolm import ffi, lib # type: ignore
@ -49,6 +50,10 @@ class OlmVerifyError(Exception):
"""libolm signature verification exception."""
class OlmHashError(Exception):
"""libolm hash calculation exception."""
class _Utility(object):
# pylint: disable=too-few-public-methods
"""libolm Utility class."""
@ -64,12 +69,12 @@ class _Utility(object):
track_for_finalization(cls, cls._utility, _clear_utility)
@classmethod
def _check_error(cls, ret):
# type: (int) -> None
def _check_error(cls, ret, error_class):
# type: (int, Type) -> None
if ret != lib.olm_error():
return
raise OlmVerifyError("{}".format(
raise error_class("{}".format(
ffi.string(lib.olm_utility_last_error(
cls._utility)).decode("utf-8")))
@ -84,18 +89,41 @@ class _Utility(object):
byte_signature = to_bytearray(signature)
try:
cls._check_error(
lib.olm_ed25519_verify(cls._utility, byte_key, len(byte_key),
ffi.from_buffer(byte_message),
len(byte_message),
ffi.from_buffer(byte_signature),
len(byte_signature)))
ret = lib.olm_ed25519_verify(
cls._utility,
byte_key,
len(byte_key),
ffi.from_buffer(byte_message),
len(byte_message),
ffi.from_buffer(byte_signature),
len(byte_signature)
)
cls._check_error(ret, OlmVerifyError)
finally:
# clear out copies of the message, which may be a plaintext
if byte_message is not message:
for i in range(0, len(byte_message)):
byte_message[i] = 0
@classmethod
def _sha256(cls, input):
# type: (Type[_Utility], AnyStr) -> str
if not cls._utility:
cls._allocate()
byte_input = to_bytes(input)
hash_length = lib.olm_sha256_length(cls._utility)
hash = ffi.new("char[]", hash_length)
ret = lib.olm_sha256(cls._utility, byte_input, len(byte_input),
hash, hash_length)
cls._check_error(ret, OlmHashError)
return bytes_to_native_str(ffi.unpack(hash, hash_length))
def ed25519_verify(key, message, signature):
# type: (AnyStr, AnyStr, AnyStr) -> None
@ -109,3 +137,14 @@ def ed25519_verify(key, message, signature):
signature(bytes): The message signature.
"""
return _Utility._ed25519_verify(key, message, signature)
def sha256(input_string):
# type: (AnyStr) -> str
"""Calculate the SHA-256 hash of the input and encodes it as base64.
Args:
input_string(str): The input for which the hash will be calculated.
"""
return _Utility._sha256(input_string)

View file

@ -43,6 +43,7 @@ ffibuilder.set_source(
#include <olm/inbound_group_session.h>
#include <olm/outbound_group_session.h>
#include <olm/pk.h>
#include <olm/sas.h>
""",
libraries=["olm"],
extra_compile_args=compile_args,
@ -54,5 +55,8 @@ with open(os.path.join(PATH, "include/olm/olm.h")) as f:
with open(os.path.join(PATH, "include/olm/pk.h")) as f:
ffibuilder.cdef(f.read(), override=True)
with open(os.path.join(PATH, "include/olm/sas.h")) as f:
ffibuilder.cdef(f.read(), override=True)
if __name__ == "__main__":
ffibuilder.compile(verbose=True)

99
python/tests/sas_test.py Normal file
View file

@ -0,0 +1,99 @@
from builtins import bytes
import pytest
from olm import OlmSasError, Sas
MESSAGE = "Test message"
EXTRA_INFO = "extra_info"
class TestClass(object):
def test_sas_creation(self):
sas = Sas()
assert sas.pubkey
def test_other_key_setting(self):
sas_alice = Sas()
sas_bob = Sas()
assert not sas_alice.other_key_set
sas_alice.set_their_pubkey(sas_bob.pubkey)
assert sas_alice.other_key_set
def test_bytes_generating(self):
sas_alice = Sas()
sas_bob = Sas(sas_alice.pubkey)
assert sas_bob.other_key_set
with pytest.raises(OlmSasError):
sas_alice.generate_bytes(EXTRA_INFO, 5)
sas_alice.set_their_pubkey(sas_bob.pubkey)
with pytest.raises(ValueError):
sas_alice.generate_bytes(EXTRA_INFO, 0)
alice_bytes = sas_alice.generate_bytes(EXTRA_INFO, 5)
bob_bytes = sas_bob.generate_bytes(EXTRA_INFO, 5)
assert alice_bytes == bob_bytes
def test_mac_generating(self):
sas_alice = Sas()
sas_bob = Sas()
with pytest.raises(OlmSasError):
sas_alice.calculate_mac(MESSAGE, EXTRA_INFO)
sas_alice.set_their_pubkey(sas_bob.pubkey)
sas_bob.set_their_pubkey(sas_alice.pubkey)
alice_mac = sas_alice.calculate_mac(MESSAGE, EXTRA_INFO)
bob_mac = sas_bob.calculate_mac(MESSAGE, EXTRA_INFO)
assert alice_mac == bob_mac
def test_cross_language_mac(self):
"""Test MAC generating with a predefined key pair.
This test imports a private and public key from the C test and checks
if we are getting the same MAC that the C code calculated.
"""
alice_private = [
0x77, 0x07, 0x6D, 0x0A, 0x73, 0x18, 0xA5, 0x7D,
0x3C, 0x16, 0xC1, 0x72, 0x51, 0xB2, 0x66, 0x45,
0xDF, 0x4C, 0x2F, 0x87, 0xEB, 0xC0, 0x99, 0x2A,
0xB1, 0x77, 0xFB, 0xA5, 0x1D, 0xB9, 0x2C, 0x2A
]
bob_key = "3p7bfXt9wbTTW2HC7OQ1Nz+DQ8hbeGdNrfx+FG+IK08"
message = "Hello world!"
extra_info = "MAC"
expected_mac = "2nSMTXM+TStTU3RUVTNSVVZUTlNWVlpVVGxOV1ZscFY"
sas_alice = Sas()
sas_alice._create_sas(bytes(alice_private), 32)
sas_alice.set_their_pubkey(bob_key)
alice_mac = sas_alice.calculate_mac(message, extra_info)
assert alice_mac == expected_mac
def test_long_mac_generating(self):
sas_alice = Sas()
sas_bob = Sas()
with pytest.raises(OlmSasError):
sas_alice.calculate_mac_long_kdf(MESSAGE, EXTRA_INFO)
sas_alice.set_their_pubkey(sas_bob.pubkey)
sas_bob.set_their_pubkey(sas_alice.pubkey)
alice_mac = sas_alice.calculate_mac_long_kdf(MESSAGE, EXTRA_INFO)
bob_mac = sas_bob.calculate_mac_long_kdf(MESSAGE, EXTRA_INFO)
bob_short_mac = sas_bob.calculate_mac(MESSAGE, EXTRA_INFO)
assert alice_mac == bob_mac
assert alice_mac != bob_short_mac

View file

@ -0,0 +1,29 @@
import base64
import hashlib
from future.utils import bytes_to_native_str
from hypothesis import given
from hypothesis.strategies import text
from olm import sha256
from olm._compat import to_bytes
class TestClass(object):
@given(text(), text())
def test_sha256(self, input1, input2):
first_hash = sha256(input1)
second_hash = sha256(input2)
hashlib_hash = base64.b64encode(
hashlib.sha256(to_bytes(input1)).digest()
)
hashlib_hash = bytes_to_native_str(hashlib_hash[:-1])
if input1 == input2:
assert first_hash == second_hash
else:
assert first_hash != second_hash
assert hashlib_hash == first_hash