aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDamir Jelić <poljar@termina.org.uk>2019-04-10 15:18:07 +0200
committerDamir Jelić <poljar@termina.org.uk>2019-04-10 15:18:07 +0200
commit086133f39a175a72ea7c898c708fed38b8dbd36b (patch)
treef335cf726acafae819d1b824c6b23186eddaa9df
parentc79d9282dcb9946144905a93ec9b5f14b17cc30c (diff)
parent54cb52e05e0d715c9e3c71ffeff05050732dea41 (diff)
Merge branch 'python-sas'
-rw-r--r--python/MANIFEST.in1
-rw-r--r--python/Makefile5
-rw-r--r--python/olm/__init__.py3
-rw-r--r--python/olm/sas.py257
-rw-r--r--python/olm/utility.py57
-rw-r--r--python/olm_build.py4
-rw-r--r--python/tests/sas_test.py99
-rw-r--r--python/tests/utils_test.py29
8 files changed, 444 insertions, 11 deletions
diff --git a/python/MANIFEST.in b/python/MANIFEST.in
index db6309d..824b377 100644
--- a/python/MANIFEST.in
+++ b/python/MANIFEST.in
@@ -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
diff --git a/python/Makefile b/python/Makefile
index 7f0121d..e4d0611 100644
--- a/python/Makefile
+++ b/python/Makefile
@@ -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
diff --git a/python/olm/__init__.py b/python/olm/__init__.py
index 7b7423b..d92b0ab 100644
--- a/python/olm/__init__.py
+++ b/python/olm/__init__.py
@@ -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
diff --git a/python/olm/sas.py b/python/olm/sas.py
new file mode 100644
index 0000000..c12b7bc
--- /dev/null
+++ b/python/olm/sas.py
@@ -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))
diff --git a/python/olm/utility.py b/python/olm/utility.py
index 121ff63..10d5ab4 100644
--- a/python/olm/utility.py
+++ b/python/olm/utility.py
@@ -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)
diff --git a/python/olm_build.py b/python/olm_build.py
index 97ab3b2..0606337 100644
--- a/python/olm_build.py
+++ b/python/olm_build.py
@@ -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)
diff --git a/python/tests/sas_test.py b/python/tests/sas_test.py
new file mode 100644
index 0000000..9001e67
--- /dev/null
+++ b/python/tests/sas_test.py
@@ -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
diff --git a/python/tests/utils_test.py b/python/tests/utils_test.py
new file mode 100644
index 0000000..a552d12
--- /dev/null
+++ b/python/tests/utils_test.py
@@ -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